Architecture
Introduction
Ocman is a single Go binary that serves a React SPA and acts as a control plane for coding-agent sessions. Four diagrams follow: the system context (what ocman talks to), the backend composition (the Go packages), the session/event data flow, and the frontend composition. Each diagram is capped at roughly 10 blocks, and the detail lives in the text below it.
Cross-machine conversation sharing
Ocman can publish a shared conversation through the standalone ocman-relay
binary. The relay stores only AES-256-GCM ciphertext through the share.Store
abstraction (disk today; object storage can implement the same
Put/Get/List/DeletePrefix contract). The per-share key stays in the URL
fragment, so it never reaches the relay.
flowchart LR
Owner[Owner ocman] -->|sealed completed turns| Relay[ocman-relay]
Relay --> Store[(share.Store)]
Viewer[Browser viewer] -->|ciphertext poll| Relay
Viewer -->|WebCrypto decrypt| Thread[Shared conversation]
Thread -->|explicit local fork| Local[Recipient ocman]
- Chunk zero is a complete snapshot; later chunks contain the latest completed turn and the viewer merges them as id-keyed upserts.
- The owner allocates sequence numbers. Nonces derive from those sequence numbers, and the share id plus sequence is authenticated as GCM AAD.
- Revocation deletes the relay prefix. The relay stores only a hash of its append/delete token and needs no database.
- Forking fetches and decrypts in the recipient’s authenticated local ocman UI, lets the recipient choose a local project, and parks the transcript as an unsent composer draft. Imported text never runs automatically.
1. System context
Everything external that the ocman process touches.
flowchart LR
Browser[Browser SPA<br/>REST + SSE] --> Ocman[ocman<br/>Go binary :8228]
Agent[AI agents<br/>MCP clients] -->|/mcp| Ocman
Ocman -->|read-only SQLite| OCDB[(opencode.db)]
Ocman -->|read/write SQLite| StateDB[(state.db)]
Ocman -->|Authenticated HTTP/SSE proxy| OCInst[Running OpenCode<br/>instances]
Ocman -->|exec| Shell[git / tmux / lsof / bd<br/>host tools]
Ocman -->|REST| Forges[GitHub / Forgejo]
Ocman <-->|gRPC + token| Remotes[Remote ocman<br/>instances]
Ocman -.->|OTLP, optional| Otel[Telemetry collector]
- Browser SPA. The only UI. It talks REST/SSE to the hub, never to
remotes directly.
:8228is the production-addrdefault; in dev the Vite server on :8228 proxies/apito the air backend on :8229. - opencode.db. Foreign data, opened read-only. Ocman never writes to it.
- state.db. Ocman’s own state: archive flags, routines and run history,
permission approval provenance, settings, Factory records, and remote
tokens. Legacy
workflow_*rows remain inert for manual recovery. - Remote ocman instances. The hub dials remotes over gRPC and re-exposes their sessions and hosts transparently. The owning remote enriches session detail with its persisted approvals and tees synthetic approval events into the gRPC event stream before the hub forwards them to the browser.
2. Backend composition
The Go package graph, collapsed to the seams that matter.
flowchart TD
Server[internal/server<br/>HTTP, SSE, handlers] --> Registry[platforms.Registry<br/>session seam]
Server --> Router[hostsvc.Router<br/>host/dir seam]
Server --> Routines[internal/routines<br/>saved prompts + schedules]
Server --> Factory[internal/factory<br/>native Issue graph + dispatch]
Factory --> FactoryModel[internal/factory/model<br/>shared persistence records]
Factory --> State
State --> FactoryModel
Factory --> Registry
Factory --> Router
Routines --> Registry
Routines --> Router
Routines --> State
Server --> MCP[internal/mcp<br/>MCP tools]
MCP --> Factory
MCP --> Routines
MCP --> Registry
Registry --> OC[platforms/opencode + internal/db<br/>adapter and read-only queries]
Registry --> RP[remote.Platform<br/>gRPC-backed]
Router --> Local[hostsvc/local<br/>git, tmux, worktree, Beads, runtimes]
Server --> State[internal/state<br/>state.db]
Server --> Forge[forge + integrations<br/>GitHub/Forgejo clients]
- internal/server. The HTTP mux, SSE broadcast and fanout, around 60 handler files, plus tmux, terminal, whisper, auto-approve and routine ticks.
- internal/factory. The independent Software Factory boundary. It stores
Epics, Mols, typed Issues, dependencies, attempts, Formula revisions,
Plan revisions, approvals, and materialization provenance in
state.db. TOML Formulas compile to canonical JSON. A Plan session is read-only at the project root; approval of an exact revision enables user-requested atomic materialization of the proposed Implementation Issues and dependencies. Ready Issues launch configured worktree sessions. The browser uses REST while agents use the one action-basedfactoryMCP tool. Routines are not involved. - Factory persistence. Native
factory_*tables own the graph and its provenance instate.db; they do not reference or alter routine tables. - internal/factory/model. Dependency-neutral persistence records shared by
internal/factoryandinternal/state. Factory owns their meaning; state only stores them, which avoids making Factory depend on its SQLite adapter. - platforms.Registry. The session-scoped seam. One adapter per platform; remotes register as compound-ID platforms so handlers can’t tell local from remote.
- hostsvc.Router. The directory-scoped seam (git, worktrees, tmux, Beads,
projects and forge-repository detection/fetch). It resolves the owning host
and delegates, the same transparency
trick as the registry. Worktree sessions run in-app on the project’s single
opencode instance, one per project, ensured through
EnsureProjectOpencode, with a per-session working directory. There is no opencode or tmux process per worktree.EnsureProjectOpencodeResultis runtime-neutral: callers use the fullEndpointURL (or itsPort()) plus an opaqueocruntime.Instance. The owning host may use discovery once to adopt a healthy instance that started before its managed registry entry existed.RestartProjectOpencodestops and relaunches the tracked instance. - internal/ocruntime. The runtime abstraction behind the managed launch
path. A
Runtimeinterface (Launch/Probe/Stop) hides how a project’s opencode is hosted; the native-tmux implementation runsopencode --port Non an ocman-allocated loopback port and probes authenticatedGET {endpoint}/configfor health. It is where the container runtime (epic #375) plugs in as a second implementation. - platforms/opencode. Wraps the read-only DB queries (
internal/db) plus an HTTP client that attaches to live instances, withlsof-based discovery for instances started outside ocman. One process-wide/global/eventstream per instance keeps pending permission and question state in memory across all session directories. - internal/routines. Validates and stores manual, timeout, one-time and
cron routines. Manual and scheduled dispatch share the durable occurrence
claim, launch a fresh managed session through
hostsvc.Routerandsessionsvc, and leave the run active until platform-neutral session status reports success or failure. Startup resumes observation of linked runs. Timeout schedules become an absolute due time when saved. Cron schedules use a five-field expression and IANA timezone. Webhooks are deferred. - Legacy Workflow storage. The
workflow_*tables remain instate.dbas inert historical data. No API, MCP tool, UI, or scheduler reads them. A DAG cannot be converted losslessly to one routine prompt, so recovery is a manual read-only SQLite export. - internal/mcp. MCP handlers expose action-based
factoryandroutines, read-only session inspection, andembed_file. File embedding uses signed tokens persisted instate.db. - internal/opencodeskills. Extracts binary-embedded ocman skills into XDG data and installs only ocman-owned symlinks for OpenCode discovery. Retirement unlinks only the exact verified symlink and preserves extracted data.
- internal/state. Ocman’s writable SQLite store: migrations, settings, routines and their immutable run snapshots, legacy inert Workflow rows, and the independent native Factory Issue graph.
- forge and integrations. Forge-agnostic types in
internal/forge, per-forge HTTP clients ininternal/forge/{github,forgejo}. PR/Issue handlers obtain repository identity from the owner Host, then use the hub clients for metadata.
3. Session and event data flow
How session reads, live updates, and routine dispatch travel through the system.
sequenceDiagram
participant B as Browser
participant S as server handlers
participant Q as routines.Service
participant R as Registry/Router
participant O as Remote owner
participant A as opencode adapter
participant D as opencode.db / OC HTTP
participant E as SSE broadcast
B->>S: GET /api/sessions
S->>R: resolve platform/host
R->>A: ListSessions()
D-->>A: background /global/event prompt updates
A->>D: SQL json_extract
A->>A: overlay pending prompt registry
D-->>B: JSON (status settled at query time)
R->>O: gRPC Session / StreamEvents (remote only)
O->>A: Session / ProxyEvents
O->>O: inject persisted approvals,<br/>tee synthetic approval events
O-->>S: enriched JSON / framed SSE
Note over S,Q: every 5 s: claim due routines
Q->>R: ensure project instance and create fresh session
R->>A: create session and send saved prompt
Note over Q,A: poll linked session until it settles
E-->>B: SSE (session.updated)
The key property: ocman never persists session status. The live turn signal from the running OpenCode instance decides whether a session is busy; the last stored message row only settles which terminal state a finished session is in. Nothing is written back, so there is no sync problem with OpenCode’s DB.
Ocman owns routine state. The Routines page creates, edits, soft-deletes, and
starts routines over REST. Manual and scheduled paths first claim an immutable
run snapshot in state.db, then create and prompt a fresh session through the
shared session service. The run stays active until that session settles. The
same row stores success or failure, any error, and the session link shown in
history.
4. Frontend composition
flowchart TD
Pages[pages/<br/>routes] --> Comp[components/<br/>~80 components]
Pages --> Stores[Client state<br/>TanStack Query + Zustand]
Comp --> Stores
Stores --> API[lib/ API client]
Stores --> SSE[SSE subscription]
Pages --> Scopes[Ref-counted activity scopes]
Comp --> Scopes
Scopes --> Reporter[Client activity lease reporter]
Reporter --> API
API -->|/api| Hub[ocman backend]
SSE -->|events| Hub
Comp --> Caps[useCapabilities<br/>capability gating]
- Capability gating. The UI never branches on platform identity. Features
toggle via
/api/capabilities, enforced by a lint script. - Client state. Shared Zustand stores hold broad session state. The Routines page loads definitions and history over REST and keeps its form and selected edits locally.
- Activity leases. Mounted data subscriptions ref-count their scopes. One authenticated reporter renews the visible tab’s lease, allowing the backend to skip view-serving session, project and metrics refreshes when every tab is hidden or gone. Scheduled routines and auto-approve do not consult these leases.
- Beads status. The right panel queries the repository owner’s
hostsvc.Hostthrough/api/project/beads-status; remote owners proxy the same operation over gRPC. Ticket data stays in the repository and is polled only while the available pane is open. - Factory.
/factorypresents actionable approval Gates, Epics, Issues, Queue, and Configuration through TanStack Query. Browser mutations create native Epics, pour Mols, decide exact Plan revisions, and explicitly close completed containers. The dispatcher records attempts before launching the read-only planning or configured implementation session.