HOST / CLIENT SPLIT · SYSTEM ARCHITECTURE

CodePapr is not a monolith — it is a host daemon plus several thin clients

Domain logic lives in the library crate codepapr-core. The only process that serves it is the codepapr-server host daemon; the desktop app (Tauri) and the CLI are both JSON-RPC clients. Rationale: docs/adr/ADR-012; full detail: docs/ARCHITECTURE.en.md.

Process topology

Four kinds of executables: clients (codepapr / codepapr-cli), the host (codepapr-server), the Node agent sidecar (agent-runtime.mjs, launched by the host), and the child processes the host manages (LSP servers, shells, MCP stdio servers).

Desktop client codepapr             CLI codepapr-cli
React+Monaco · @codepapr/core       ping/doctor/status/git/fs/
src-tauri/src/host.rs (RPC client)  shell/lsp/server/chat
        │   JSON-RPC 2.0, line-delimited        │
        │   TCP 127.0.0.1 (or stdio)            │
        └────────────────┬─────────────────────────┘
                          ▼
           codepapr-server (host)
           handler.rs → initialize / ping + 159 namespaced methods
           tasks.rs  → serial task queue
                          ▼
           codepapr-core (domain library, no Tauri dependency)
           workspace_fs · git_operations · shell · lsp
           symbols · db(SQLite) · snapshot · mcp_host · web
           agent_runtime → Node agent-runtime.mjs sidecar
STARTUP

Attach or spawn

CODEPAPR_SERVER_URL set → connect only, no child process. Otherwise locate the binary (CODEPAPR_SERVER_BIN → next to the executable → resourceDir → target/{debug,release} → PATH) and run codepapr-server --port 0 --port-file <temp>/codepapr-server-<pid>.port; the port is read from the port file or the stderr line [codepapr-server] listening on (15s deadline, 40ms poll), then initialize is sent.

SHUTDOWN

Fixed order

lsp/stopAll → agent/stopAll → fs/stopWatcher → shell/stopAllBackground → mcp/disconnectAll → kill + wait (only for a self-spawned child; an external host via CODEPAPR_SERVER_URL is left running). The desktop waits for settings to flush first (polls db/settingsSaveState).

LayerOwnsDoes not own
UI (React)Interaction, panels, driving the agent loop, prompt assemblyAny direct file / process / database access
Tauri clienthost.rs RPC client, commands.rs proxies; GUI-only: codepapr-app://, embedded/headless browser, TTS, Stronghold vault, file export, app install & port probingFilesystem, Git, shell, LSP, MCP, snapshots, web, the main SQLite DBs
codepapr-serverMethod routing, param validation, event broadcast, in-memory secrets, serial task queueDomain implementation (delegated to core)
codepapr-coreEvery domain implementation and child-process lifecycleUI, protocol layer

Two deliberate exceptions where the desktop crate bypasses RPC via a direct path dependency on codepapr-core: Papr App storage (papr_runtime/app_storage.rs calls db::papr_storage_* directly) and secrets (plaintext lives only in the client Stronghold vault, pushed one-way to the host's in-memory store via secrets/import; the host never persists secrets).

JSON-RPC surface

Line-delimited JSON-RPC 2.0, one object per line. --port <N> → TCP (--port 0 lets the kernel pick, --port-file writes the bound port); no --port → stdio. Requests with an id get a response; without one they are notifications. On disconnect, every pending request fails with codepapr-server closed the connection — there is no automatic reconnect.

NamespaceMethodsRepresentative
fs/*23readTextFile · writeTextFile · listFiles · search · startWatcher
git/*8status · diff · log · stage · commit · branchCheckout
shell/*17execute · startBackground · openSession · sendCommand
lsp/*12startServer · request · diagnostics · batchSymbols
symbols/*8definition · references · hover · checkSyntax
db/*45loadSettings · saveMessageBatch · paprStorage*
snapshot/*11ensure · create · diff · restorePlan · restoreExecute
mcp/*10listTools · callTool · updateSettings · disconnectAll
agent/*5start · send · stop · respondPermission
web/*3search · fetchUrl · downloadFile
task/*2enqueue · poll
secrets/import1Populate in-memory secrets

159 namespaced methods total plus initialize / ping; anything unrecognized returns Unknown JSON-RPC method. Events ride {"method":"event","params":{"event","payload"}} notifications and are re-emitted verbatim on the Tauri event bus, names unchanged: workspace-files-changed, project-stats-progress, codepapr://lsp-managed-status, agent-runtime://frame/exit/permission-request/permission-cancel/workspace-mutated, mcp-confirm-request.

Data flow and persistence

One tool call

React → invoke() → commands.rs
  → host::call("fs/readTextFile")
  → TCP → handler.rs → validate
  → codepapr_core::workspace_fs
  → result returns to UI

One event

watcher.emit("workspace-files-changed")
  → server wraps as event notification
  → host.rs dispatch_incoming_line
  → app.emit(event, payload)
  → React listen(event, ...)
DatabasePathContents
App DB~/.codepapr/codepapr.sqliteGlobal settings, provider/model, recent workspaces
Project DB<workspace>/.CodePapr/project.sqliteSessions, checkpoints, ProjectGraph cache
Papr App DB<appDir>/db.sqliteKey-value storage and inbox for one .papr app

All three are opened by codepapr-core::db, i.e. the host process writes them (app storage is the one in-process exception). Legacy state.json/project.json import into SQLite on first open or save.

App / plugin marketplace and scope

.papr apps install from the official registry mmrqwe/codepapr-apps → registry.json into a global or workspace scope.

GLOBAL

~/.codepapr/apps/<appId>/

Cross-project tools, global permission defaults.

WORKSPACE

<workspace>/.CodePapr/apps/<appId>/

Project-specific, shippable with the repo; same appId overrides the global install so frontend/backend/storage stay on one directory.

Each app gets one origin codepapr-app://<appId>/; CSP is derived from two permission axes (local access × network) — with network off, script-src disallows https:. db.sqlite (and wal/shm) is never served over the protocol. A directory counts as an app only when its name is valid, manifest.json parses, and the entry file exists.

Context layering (TypeScript side)

Side effects are delegated to the host; prompt assembly and cache partitioning stay in @codepapr/core (TS side). Full detail: docs/adr/ADR-001…016.

LayerHoldsChanges when
01 System coreSystem prompt, tool schemas, AGENTS.mdFrozen for the session
02 Session bootstrapSkills catalog, plugin inbox summaries, project memory (MEMORY.md)Next turn after MEMORY.md is saved or compaction
03 Session stateCheckpoint (task progress) + recent turnsRewritten on compaction
04 Turn recallRetrieved procedures for this turnNew every user turn
05 Active tailCurrent user message, tool resultsAppend-only at the tail

Project memory is authoritative in the workspace file .CodePapr/MEMORY.md: the built-in memory curator maintains it at the delivery / pre-compaction checkpoints, and it is injected into 02 in full every turn (effective the next turn after saving); the Agent has no memory tools and direct writes are rejected. The panel is that file's editor.