Language selection ranked only the user's spoken languages, so a user who speaks
one language always got that language. With spoken=["en"], Portuguese audio
detected at 0.968 pt was sent as language=en, and Whisper answered with a rough
English rendering rather than a transcript. `translated` stayed false, so nothing
downstream could tell a translation had happened, and detectedLanguage reported
"en" for audio that was not English.
Take Whisper's top language when it clears 0.5, and fall back to the spoken
languages only when the clip really is ambiguous — which is the case the bias was
written for.
Verified against the local whisper server with pt/en/es recordings:
spoken=["en"] pt -> translated English, es -> translated English, en -> as-is
spoken=["en","pt"] pt -> Portuguese transcript, no translation
spoken=[] pt -> Portuguese transcript
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
OpenAI-compatible TTS servers disagree on the /v1/audio/voices payload: some
return plain strings, Kokoro returns objects like { id, name }. The handler cast
the response to { voices: string[] } without checking, so the objects reached
the voice <Select>, which renders each entry directly — React error #31, and the
whole system settings page unmounted.
Flatten to ids at the boundary, preferring id then voice_id then name, and drop
entries that yield neither. An empty result now falls through to the HuggingFace
lookup instead of returning an empty list. The ElevenLabs branch goes through the
same helper so a shape change there cannot throw either.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Onboarding was dead in three layers:
- The OnboardingAdmin screen was only reachable from a route block in App.tsx
that has been commented out, so it never rendered. Its ServerTypeCard carried
accountMode ('organization' | 'single'), inherited from the codebase this was
based on and meaningless for a single-user platform.
- Two /onboarding-complete endpoints, one public and one protected, that no
frontend code called. Both read a server_config key that was never written, so
both answered false while the app's own path defaulted to true.
- HomeScreen gated on settings.onboarding.complete to show a welcome panel, and
seedHomeDir created an Onboarding folder from DATA_PATH/Onboarding and
/Onboarding_Admin — neither seed directory exists, so it only ever produced an
empty folder.
Also drops the onboarding key from UserSettings and the now-empty home-header
panel from the default home layout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The seven files in officer_db/migrations/ were never applied — this database has
only ever been managed with db:push, so there is no __drizzle_migrations table
and the numbered history had drifted from the real schema (0001 creates a
saved_sessions table the database does not have).
Deleted so there is one story about how the schema gets applied. db:gen can
write a fresh baseline from the current schema.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The old version described a repo that no longer exists: apps/dashboard and
apps/editor, a tracking server on port 5001, an ephemeral_db, and a dashboard
and API running as separate processes. None of that is true.
Replaces it with what the code actually does — one Bun process serving the SPA,
the API and eight WebSocket providers; sidecars for the privileged work; the
split between Postgres and the file-backed items store; and the single-user
invariant stated as an invariant rather than a migration in progress.
Also records two things that are easy to get wrong from reading alone: the
database is maintained with db:push and has never had a migration applied, and
agents run unsandboxed with permissions bypassed on purpose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- DiscordAccount seeded DiscordStatus without its two nullable fields.
- bug-report typed reporter.name as string, but users.name is nullable; and the
Discord upload wrapped a Buffer directly in a Blob.
- Lucide icons take no `title` prop, so the sync spinner's tooltip moved to a
wrapping span.
- DesktopView cast its dynamic import to a type that included `| null`.
- dock PUT cast the request body straight to string[]; it now rejects anything
that is not an array of strings instead of writing it to the database.
- buildZodSchema assembles a mutable record, since z.ZodRawShape is readonly in
zod v4.
- The dev-server proxy forwards Bun's `string | Buffer` frames through a helper
that satisfies WebSocket.send without copying.
bunx tsgo is now clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every verb ended in `return data as T | any`, and `T | any` collapses to `any` —
so client.get<Foo>() handed back `any` and no annotation downstream meant
anything. That was the source of most of the implicit-any errors: the callbacks
had nothing to infer from.
Returning `as T` drops the whole class (19 errors to 9) rather than annotating
each parameter. Two calls in useAuth were relying on the looseness and now
declare their response shapes.
signin also no longer stores `undefined` as the bearer token when the server
withholds one; that path returns early.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three unrelated type errors that each pointed at something actually broken:
- Dashboard.CodeEditor was deleted in ab03b17 ("Projects") while the
/code-editor route and the dock's Editor item kept pointing at it, so the
route rendered undefined. Screen restored.
- Appearance.tsx imported 'themes', a workspace deleted in 0746844. Nothing
reads settings.appearance.colorTheme and no theme CSS survives, so the picker
was writing a value with no consumer. Removed it and the setting; colorMode
stays, it is live.
- officerdev exported both a component and a type named AppRegistry, so
`import { AppRegistry }` resolved to the type and <AppRegistry /> failed to
typecheck. Renamed the Record type to AppRegistryMap.
Also declares "*.css" so side-effect stylesheet imports resolve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
resolveUserPath and five sibling checks used startsWith(rootDir), which also
accepts a sibling directory whose name begins with the root's: from a root of
/home/br, "../br-backup/secret" resolves to /home/br-backup/secret and passed.
Compare against root + sep (or the root itself) via a shared isInside helper.
Verified the escape cases now deny while "", ".", and ordinary relative paths
still resolve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bodyParser caught parse failures and did nothing — the throw was commented out
and `body` was never set, so handlers destructured undefined and the client got
a 500 for a malformed request. Throw BAD_REQUEST instead.
Also adds the regression tests for the Host suffix match fixed in 2ca850c.
Verified against a running server: malformed JSON now 400, valid credentials
path still 401, spoofed Host still 403.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The no-Origin branch asked whether the configured origin ends with the
client-supplied Host, so `Host: dev` matched https://rezio.pastilhas.dev — as
did `pastilhas.dev` and `o.pastilhas.dev`. Match the URL authority exactly
instead. Officer always runs behind an HTTPS reverse proxy, so the forwarded
Host is expected to equal PUBLIC_URL's authority.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Origin validation, every rate limiter and the password-strength check each
treated an unset PUBLIC_BUILD_ENV as "relaxed", so a deployment that forgot the
variable silently ran with CORS reflecting any origin, no brute-force limit on
the sole account, and no password rules. setup.sh writes it, but .env.example
never mentioned it.
The three now share IS_DEV_BUILD, which is true only when PUBLIC_BUILD_ENV is
explicitly "dev" or "development". Anything else, including unset, is hardened.
Documented in .env.example.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The handler called onOpenChange and navigate, neither of which is in scope in
PipelineRunner — they belong to TaskRunnerModal, declared further down. Clicking
the button after a pipeline finished threw instead of navigating.
PipelineRunner now takes an onClose prop and calls useNavigate itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer is single-user: the server owner is the only account, created once by
/auth/bootstrap. Everything that existed to serve additional users was
unreachable, so it is gone rather than left looking like it does something.
Accounts: drop the invite / resend-invite / delete / list-users routes and the
Users settings screen, the inert /auth/signup handler, and the account
verification chain it fed (verify, resend-verification, VerifyScreen, the
UserInvite + VerifyAdmin + VerifyRegistration templates). /auth/verify-token
survives for password resets only, and now requires a reset-password token
rather than accepting any signed JWT.
Roles: drop the users.role column and the four-value USER_ROLES enum. The
permissions table granted every role identical methods, and every
role === 'Super Admin' check was permanently true. The JWT no longer carries a
role claim.
Sandbox: remove sidecar/sandbox.ts and its five call sites. bwrap was selected
only for non-Super-Admin users, so it never ran. It was also not a usable agent
jail as written — --share-net, the project root (with .env) bound read-only,
and runuser dropping to the server's own uid. Rebuilding it for agent
containment would be a different construction, and git history keeps this one.
getHomeDir keeps its DATA_PATH meaning; the new getOwnerHomeDir resolves the
owner's real login home, which is what terminals, chats and task runs use.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Claude sidecar execs /usr/local/bin/claude (claude-manager.ts), but the
Anthropic installer only puts the CLI in ~/.local/bin — so on a fresh host that
path doesn't exist and claude chat fails with
"ENOENT … posix_spawn '/usr/local/bin/claude'". Symlink ~/.local/bin/claude →
/usr/local/bin/claude after install (idempotent; tracks Claude's self-updates).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The item store location wasn't written to .env, so a fresh server fell back to
<repo>/officer-items and booted with an empty store. Prompt for it (default: a
sibling of the repo) and write it to .env.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The VNC sidecar builds its .vnc/passwd rfbauth file with `vncpasswd -f`
(vnc-manager.ts) — x11vnc alone doesn't ship vncpasswd. The GNOME-on-Xorg
rewrite dropped tigervnc, so the mirror couldn't create its password and
/desktop failed with "VNC password not configured". Add tigervnc-common back.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Collapse the two-phase bootstrap (email a verification link → verify screen) into
one direct step: the Bootstrap form collects name/email/username/password and posts
once to /bootstrap, which creates the first user directly as an active Super Admin
(+ provisions DATA_PATH/<email>). Still gated to an empty user table.
The invite flow (/verify, /verify-token) is untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the multi-user provisioning leftovers (the provision-existing-users.sh
migration was already deleted in the prior commit):
- gut provisionVncEnv from provision.ts (per-user startxfce4 virtual desktop, dead
since the switch to mirroring :0 — vnc-manager.ts self-provisions its own passwd)
- drop the Pi `.pi/agent/sessions` seed and the now-orphaned `run` helper
Wire provisioning into bootstrapHandler: the super admin (first user) is created via
createUser, which never called provisionUserEnvironment — only the invite/verify flow
did. So the single user's DATA_PATH/<email> was never provisioned up front. Now it is.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
setup-desktop.sh now installs ubuntu-desktop + gdm3 + x11vnc and forces the
Xorg session (WaylandEnable=false) with auto-login — x11vnc can only mirror an
Xorg :0, not Wayland. vnc-manager.ts resolves the X authority from the GDM
per-session path (/run/user/<uid>/gdm/Xauthority) with a ~/.Xauthority fallback.
setup.sh fixes:
- desktop step gates on `dpkg -s ubuntu-desktop` (was the decommissioned
officer-vnc service, which never matched so setup-desktop re-ran every time)
- remove Pi (install, --list-models validation, verification check)
- export GOPATH before the cliamp build so `go install` lands where it's checked
even when Go was already present this run
- write PUBLIC_BUILD_ENV=production and quote all .env values
- guard the interactive .env block behind a TTY check so non-interactive runs
skip cleanly instead of aborting on read EOF under set -e
- restart systemd-logind only when a key actually changed
- sed prefix-strip instead of `tr -d` (which deletes characters, not a prefix)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
origin-validation: the production web origin now reads from PUBLIC_URL (.env), e.g.
https://officer.pastilhas.dev, instead of a hardcoded domain; drop alpha.officer.dev.
officer-web/index.html: point og:image/favicon/manifest/etc. at the new domain (served
locally from public/).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New officer-opencode sidecar (same philosophy as officer-claude): a singleton that owns
an `opencode serve` running from DATA_PATH/opencode-sidecar (created if missing) on a
random port, registers with the API as capability 'opencode', and reports its port via a
new `opencode:server` protocol event. The API stores it (sidecar-server.ts, wired in
server.tsx via getOpenCodeServerUrl). ecosystem.config.cjs runs the sidecar instead of a
bare pm2 serve. Turn-running + API rewiring come in later steps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The prior "treat this as your cwd" wording didn't stop the model from using bare
globs/relative paths, which OpenCode resolves against the server cwd. Reworded to
explicitly require absolute paths under the target dir on every tool call (and `cd`
for bash). Still a soft override; a stronger attempt before considering `opencode run --dir`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Simpler + more efficient than the previous per-turn GET /session: the client already
has the selected cwd, so it now sends it on every message (it's constant for a session).
The server uses msg.cwd directly for OpenCode's per-turn working-directory system prompt,
and still tags the cwd at creation for listing. Drops the getSession round-trip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
msg.cwd only rides the first message, but OpenCode's system prompt is rebuilt every
turn — so turn 2+ reverted to general_chat_sessions and the model fell back to the
server's real cwd. Fix: the cwd is bound at creation (metadata.officer.cwd), so for an
existing/resumed session read it back (new client.getSession) and reuse it for the
system prompt every turn. Also make the prompt explicit that it overrides any other
working directory the environment reports.
Verified: a session created with a cwd reads the same cwd back on a later turn.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both harnesses now use the resolved chat cwd as their working directory: Claude runs
in it natively, and OpenCode is told the same via its system prompt. Removes the
earlier special-case that pointed OpenCode at the user's home for /chat, so `workingDir`
collapses into `cwd` — which now both tags the session (metadata.officer.cwd) and drives
the Officer system prompt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The default /chat working directory is used by both the Claude and OpenCode harnesses
now, so its Claude-specific name was misleading.
- Rename the dir + accessors: getClaudeSessionsCwd → getGeneralChatSessionsCwd,
ensureClaudeSessionsCwd → ensureGeneralChatSessionsCwd, path segment claude_sessions
→ general_chat_sessions (data-path on disk + code + UI labels/comments). No history
migration — the old Claude transcript slug is orphaned (intentionally).
- Remove the vestigial chat_sessions dir (leftover from the retired session store):
it only ever held empty claude/archived/ dirs, recreated by a signin hook. Drop that
hook (+ its dead imports) and the 4 unused data-path accessors (getUserSessionsDir,
getClaudeDir, getSessionDir, getArchivedSessionDir), and delete the dir.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Each pooled per-cwd server is a ~0.5GB process; across officer restarts the previous
instance's servers would orphan and pile up. On module load, kill any opencode serve on
a non-fixed port (the fixed OPENCODE_SERVER_URL port is preserved).
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>
useChat connected to `/api/chat/chat/ws` while the server listens on `/api/chat/ws`.
The double `chat` was a de-Pi rename artifact: the global `api/pi/` → `api/chat/` sed
rewrote `/api/pi/chat/ws` to `/api/chat/chat/ws`, and the targeted fix ran too late to
catch it. Result: the chat socket never connected, so the UI showed "Disconnected" and
the model selector — locked while disconnected — displayed only the active (Claude)
provider, hiding OpenCode. One-character path fix restores all chat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removes the Big Pickle + Claude Haiku allow-list; the picker now lists every model
the fixed server reports from GET /config/providers.
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>
The full `opencode models` catalog is ~58 entries; surface only opencode/big-pickle
and opencode/claude-haiku-4-5 in the picker for now via an allow-list. Easy to extend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
list-models.ts now merges the OpenCode catalog (from `opencode models`, cached;
ids are providerID/modelID) with the static Claude tiers, so /chat/models returns
both. invalidateModelCache clears the OpenCode cache for real now.
Adds the 'opencode' → 'OpenCode Zen' provider label in the /models response and the
ModelSelector's PROVIDER_DISPLAY. The existing useModels visibility gates already
pass non-claude-code providers through, so no gate changes are needed — Super Admin
sees all models. Selecting any non-claude-code model routes the turn to the OpenCode
harness (Phase 1).
Default model stays 'claude-code'. 58 OpenCode Zen models currently list; curating to
a flagship subset is an easy follow-up if the full catalog is unwieldy.
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>
Deletes all session persistence that isn't Claude's native transcript store, per
the "only harness-native session management survives" rule.
Backend: delete api/pi/storage.ts (meta.json+messages.json file store), the
api/saved-sessions router (+ unmount), the /pi/sessions REST endpoints, and the
storage.save/loadSession calls in the chat WS handler (in-memory session-manager
stays for live turns; no disk persistence — Claude's transcript is the record).
Also drops the Postgres saved_sessions layer: schema/chat.ts, queries/saved-sessions.ts,
its types and re-exports.
Frontend: delete state/useSavedSessions, ChatList, and the ChatHistory Widget
(all pure saved-session UI); slim ChatHeader to a label; strip the auto-load-latest
+ Save wiring from ChatPanelWrapper and ChatDetailPanel; drop the old resume path
from useChat and SessionListPage; remove the /chat/saved/:id route and the
useInitialData prefetch.
Behavior removed (intended): the Save-session button, email/project panels
auto-resuming the last chat, and /chat/saved/:id. /chat itself is unchanged —
already fully on Claude transcripts. The orphaned saved_sessions Postgres table
is dropped on the next `bun db:push`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Frontend de-Pi (Stage 3). Renames the chat hook usePiChat → useChat (+ UsePiChatType
→ UseChatType, file moved to hooks/useChat.ts) across all consumers, and renames
the TaskRunnerModal agentic runner PiMonoInner → AgenticTaskRunner (dropping the
dead defaultProvider === 'pi' check → always the Claude default). Pure rename, no
behavior change. The WS route stays /api/pi/chat/ws until Stage 4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Orphaned standalone email-assistant chat, superseded by the officerdev/chat panel
(ChatPanelWrapper) that the email layout uses. Imported nowhere.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bun closes a WS idle for idleTimeout (60s) and only resets its timer on frames
received from the client — but during a chat turn the client only receives, so a
long tool call or a gap between turns tripped the timeout (clean close 1000,
mid-stream cuts, reconnects). Now each chat connection is pinged every 25s in the
WS open handler (cleared in close); the client auto-pongs at the protocol level,
resetting Bun's timer. Covers every client (web, mobile) with one server change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deletes the Automation and NewAutomation screens, their /automation and
/new-automation routes, the Dashboard barrel exports, the dock item (+ default
dock path), and the page-title rule. TaskRunnerModal is kept — the file browser
still uses it to run tasks on files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>