# CodePilot source analysis

> **A multi-model AI agent desktop client** · Electron 40 + Next.js 16 + SQLite + Claude Agent SDK + Native Runtime + Codex Runtime
>
> commit `v0.62.0` · about 1266 product TS/TSX (about 1448 including `资料/` vendor) · BUSL-1.1
>
> Every claim is tagged `file:line`, traceable to concrete source.

---

## I. Project overview

### 1.1 What CodePilot is

CodePilot is not another coding agent. It is a **multi-model AI agent desktop client** — a Next.js app wrapped in an Electron shell, moving “talk to AI” from the terminal onto the desktop, and expanding “conversation” into a “general agent platform”:

- **17+ AI providers** (Anthropic / OpenAI / Google / xAI / GLM / Kimi / DeepSeek / Ollama / Bedrock / Vertex…)
- **Three Runtimes** (Claude Code SDK / Native first-party / Codex app-server)
- **Remote IM Bridge** (Telegram / Feishu / Discord / QQ / WeChat)
- **MCP + Skills ecology**
- **Media generation** (Gemini images)
- **Task scheduling** (cron)
- **Session replay** (rewind to any checkpoint)

**Its relation to Claude Code**: Claude Code is a coding agent in the terminal, reading your input line by line, calling tools to change files. CodePilot is a general agent client on the desktop — it **reuses Claude Code’s SDK** as one of three Runtimes, while also shipping a Native Runtime for non-Claude models, and attaching OpenAI Codex as the third Runtime.

**Its relation to open-design**: open-design is also a “host that does not write the agent loop,” but the host it builds is **design** (generating HTML/PPT). CodePilot builds a **general agent desktop** — chat, coding, tasks, media, remote control, all stuffed into one Electron window.

### 1.2 The project’s shape in numbers

| Dimension | Number | Notes |
|---|---|---|
| Source files | about 1266 product TS/TSX (about 1448 including 资料) | excluding node_modules; `资料/` is WeChat/Feishu vendor |
| Total files | 2522 | including JSON/MD/images etc. |
| Core engine | claude-client.ts 3628 lines | SDK wrap + message stream |
| Native loop | agent-loop.ts 1019 lines | while-loop driven |
| Database | db.ts 5752 lines | SQLite schema + migrations |
| API endpoints | about 170+ route handlers (about 29 first-level domains) | the one that actually runs the Agent is `POST /api/chat`; `/api/chat/messages` only persists |
| Bridge adapters | 5 | Telegram/Feishu/Discord/QQ/WeChat |
| Provider presets | 17+ | provider-catalog.ts 2400 lines |
| Skill directory | public/skills/ | compatible with Claude Code format |
| License | BUSL-1.1 | commercial use restricted |

### 1.3 The essential difference among the three Runtimes

This is CodePilot’s most core architectural decision — **the same frontend, three different Agent engines**:

| Runtime | Engine | Main-loop location | Scene |
|---|---|---|---|
| **claude-code-sdk** | `@anthropic-ai/claude-agent-sdk` | inside the SDK child process | Claude models, full Claude Code capability |
| **native** | first-party while loop + Vercel AI SDK | `agent-loop.ts:373` | non-Claude models (GLM/Kimi/DeepSeek…) |
| **codex** | `codex app-server` child process | inside Codex | OpenAI Codex models |

The three Runtimes share the same frontend, the same database, the same permission system, the same Bridge — but **the main-loop implementations are completely different**. That is CodePilot’s most distinctive architectural decision: **Runtime is swappable**.

### 1.4 The fundamental difference vs Claude Code

| Dimension | Claude Code | CodePilot |
|---|---|---|
| UI | terminal TUI | Electron desktop GUI |
| Models | locked to Anthropic | 17+ providers |
| Runtime | single (first-party) | three, swappable |
| Main loop | first-party while loop | SDK outsourced / Native first-party / Codex outsourced |
| Tools | hardcoded builtins | standard AI SDK ToolSet |
| Permissions | five-gate adjudication | three layers (rule engine + popup + Profile) |
| Sessions | terminal session | SQLite persist + rewind |
| Remote | none | IM Bridge |
| Sub-agents | Task tool | subagent_runs durable lifecycle |

---

## II. Panorama

### 2.1 Process topology

```
┌─────────────────────────────────────────────────────────┐
│  Electron 主进程 (electron/main.ts, 2566 行)             │
│  ├── 窗口管理 (BrowserWindow)                            │
│  ├── utilityProcess.fork → Next.js standalone server     │
│  ├── TerminalManager (pty)                               │
│  ├── Tray / 通知 / 文件对话框                             │
│  └── 日志轮转 (50MB×5)                                   │
└────────────────────┬────────────────────────────────────┘
                     │ HTTP/SSE (localhost)
┌────────────────────▼────────────────────────────────────┐
│  Next.js 16 Server (App Router)                          │
│  ├── src/app/api/ — 约 170+ route handlers（约 29 一级域） │
│  │   ├── /api/chat — 聊天（消息/中断/权限）              │
│  │   ├── /api/media — 图片生成                          │
│  │   ├── /api/settings — 设置                           │
│  │   ├── /api/providers — Provider 管理                 │
│  │   ├── /api/tasks — 任务调度                          │
│  │   └── …                                              │
│  ├── src/lib/ — 核心业务逻辑                             │
│  │   ├── claude-client.ts — SDK 封装（3628 行）         │
│  │   ├── agent-loop.ts — Native 循环（1019 行）         │
│  │   ├── stream-session-manager.ts — SSE 流管理         │
│  │   ├── conversation-registry.ts — 会话注册表          │
│  │   ├── db.ts — SQLite（5752 行）                      │
│  │   ├── runtime/ — Runtime 注册表 + 三实现              │
│  │   ├── tools/ — 8 个编码工具                          │
│  │   ├── builtin-tools/ — 平台工具                      │
│  │   ├── permission/ — 三层权限                         │
│  │   ├── bridge/ — IM Bridge 子系统                     │
│  │   ├── channels/ — 渠道插件                           │
│  │   ├── codex/ — Codex 集成                            │
│  │   └── harness/ — 能力契约 + 上下文编译               │
│  └── src/components/ — React 组件                        │
│      ├── chat/ — 聊天界面                               │
│      ├── ai-elements/ — AI 响应渲染                     │
│      ├── layout/ — 布局                                 │
│      └── …                                              │
└────────────────────┬────────────────────────────────────┘
                     │ SQLite (better-sqlite3, WAL)
┌────────────────────▼────────────────────────────────────┐
│  ~/.codepilot/codepilot.db                              │
│  ├── chat_sessions / messages                           │
│  ├── subagent_runs / subagent_run_events                │
│  ├── api_providers / settings / tasks                   │
│  ├── media_generations / media_tags / media_jobs        │
│  └── channel_bindings / channel_offsets                 │
└─────────────────────────────────────────────────────────┘
```

### 2.2 Four boundaries you must remember

**① Electron is only the shell.** `electron/main.ts` uses `utilityProcess.fork` to start the packed Next.js standalone server (`serverProcess`), listens on a free port, and after `GET /api/bridge` health-check passes, `mainWindow.loadURL(http://127.0.0.1:${port})`. Electron is unaware of any Agent logic — it only owns windows, terminals (pty), notifications, file dialogs.

**② The three Runtimes share the same infrastructure.** `resolveRuntime()` in `runtime/registry.ts:72-152` (the file is 198 lines; L21 is only the Map declaration) selects Runtime in five levels: “Codex explicit → explicit override → cli_enabled=false → global settings → auto.” After selection, frontend, DB, permissions, Bridge are all reused.

**③ The SDK path’s “main loop” is an event pump, not a true loop.** `for await (const message of conversation)` at `claude-client.ts:2239` only translates SDK-child-process events into SSE. The real “model→tools→feed back→model” loop happens inside the SDK child (claude CLI). The CodePilot application layer only does reactive forwarding + guardrails.

**④ The Native path is the true loop.** `while (step < maxSteps)` at `agent-loop.ts:373` manually calls `streamText()` each step, inserting permission checks, DB persist, timeout budgets, context trim, SSE forwarding between steps — hooks the AI SDK’s automatic mode cannot attach.

### 2.3 Data flow: from user input to AI response

```
用户输入 → MessageInput / startStream
  → POST /api/chat          ← 真正触发 Agent（chat/route.ts）
  → stream-session-manager 会话锁（chat 路径常传 600s TTL；锁默认常量另有 300s）
  → streamClaude()（claude-client.ts）解析 Provider + Runtime
  → addMessage 入库 + assembleContext（含 context-compressor / context-compiler）
  → runtime.stream() 分流：
    ├── SDK: streamClaudeSdk() → SDK query() 子进程
    ├── Native: runAgentLoop() → while (step < maxSteps) → streamText()
    └── Codex: getCodexAppServer() → app-server → thread/start → turn/start
  → SSE 流式返回 → useSSEStream → MessageList
  → db.ts 持久化到 SQLite

说明：POST /api/chat/messages 只持久化消息、不跑模型（图片生成等模式用）。
```

**Bridge data flow (remote IM control):**
```
Telegram/飞书消息
  → Adapter 长轮询/WebSocket 接收
  → channelRouter 路由到 CodePilot session
  → conversationEngine 调用 SDK
  → SDK SSE 响应
  → deliveryLayer 格式化 + 分片
  → Adapter 发送回 IM
```

---

## III. Startup

### 3.1 Electron main-process startup

Startup sequence of `electron/main.ts` (2566 lines):

1. **Sentry init** (can opt-out)
2. **`utilityProcess.fork`** starts the packed Next.js standalone server
   - listen on a free port (`serverProcess`)
   - `GET /api/bridge` health check
   - SIGTERM → 3s → SIGKILL shutdown
3. **`mainWindow.loadURL(http://127.0.0.1:${port})`**
4. **TerminalManager** (pty) init
5. **Tray / notifications / file dialogs** registered
6. **Log rotation** (50MB×5)

**Key design**: Electron main process and Next.js server are **two independent processes**, communicating over HTTP/SSE. In Dev mode it connects directly to the `next dev` dev server; in Prod it uses `utilityProcess.fork` to start the standalone server.

### 3.2 Next.js Server startup

When Next.js 16 App Router starts:
1. `src/lib/db.ts` initializes SQLite (WAL + foreign keys + busy_timeout=5000)
2. `src/lib/runtime-log.ts` initializes a console ring buffer (200 entries, auto-redact)
3. `src/lib/bridge/bridge-manager.ts` initializes the Bridge manager (globalThis singleton, HMR-resistant)
4. `src/lib/conversation-registry.ts` initializes the active-session registry

### 3.3 Database init

`initDb()` at `db.ts:186`:

```sql
-- 核心表
chat_sessions (id, title, model, provider_id, sdk_session_id, …)
messages (id, session_id, role, content, …)
subagent_runs (id, parent_session_id, status, …)
subagent_run_events (id, run_id, event_type, …)
settings (key, value)
tasks (id, session_id, title, status, …)
api_providers (id, name, protocol, base_url, …)

-- 媒体表
media_generations / media_tags / media_jobs / media_job_items / media_context_events

-- Bridge 表
channel_bindings / channel_offsets / permission_link / …
```

**Migration mechanism**: `withMigrationLock` (`db.ts:84`) uses `O_CREAT|O_EXCL` file lock + 10s retry, preventing concurrent migration from multiple Next.js workers. `migrateDb` (`db.ts:482`) uses `PRAGMA table_info` probe + `safeAddColumn` for idempotent `ALTER TABLE` incremental columns.

---

## IV. Input capture and routing

### 4.1 Chat input

`src/components/chat/MessageInput.tsx` captures user input → via `startStream` goes `POST /api/chat` (the one that actually triggers the Agent). `POST /api/chat/messages` only persists, does not run the model.

### 4.2 API route handling

Full flow of `src/app/api/chat/route.ts` (990-line POST):

1. **Validate body** (session_id, content, model, …)
2. **Pre-check**: `hasCodePilotProvider` (412 NEEDS_PROVIDER_SETUP)
3. **Session lock**: `acquireSessionLock` (409 SESSION_BUSY, 600s TTL + renew/watchdog)
4. **Resolve Provider + Runtime**: `resolveRuntime()` five-level select
5. **Message persist**: `addMessage`
6. **Context assembly**: `assembleContext` (including context-compressor)
7. **Route to Runtime**:
   - SDK: `streamClaudeSdk()`
   - Native: `runAgentLoop()`
   - Codex: `codex app-server`
8. **SSE stream return**
9. **Background persist**: `collectStreamResponse` (continues even if the renderer disconnects)
10. **Release lock**

### 4.3 Session lock

The `session_runtime_locks` table (`db.ts:891`) implements session-level mutual exclusion:

- `acquireSessionLock`: acquire the lock, 600s TTL
- `isLockOwner`: verify lock ownership (`lockId`)
- renew/watchdog: prevent lock expiry causing concurrency issues

**Why a lock is needed**: the same session cannot have two streams running at once — otherwise message order would scramble.

### 4.4 Interrupt

`/api/chat/interrupt`:
- explicit Stop: abort the parent turn and propagate to children
- `stopStreamWith` at `stream-session-manager.ts:1046`: first unconditionally arm a 2s force-abort safety net (#578 fix), then best-effort call `/api/chat/interrupt`

---

## V. Context assembly

### 5.1 assembleContext

Context assembly is “pack user input + history + system prompt + tool descriptions” into the request sent to the model.

Key components:
- **`agent-system-prompt.ts`**: build the system prompt
- **`harness/context-compiler.ts`**: pure function `compileContext(input)` — given enabled capabilities + user/external extension scan results, produce an ordered, deduped system prompt + tool surface + artifact contract
- **`context-compressor`**: context compact

### 5.2 Harness capability contract

`harness/` is one of CodePilot’s most distinctive subsystems — **capability contract + context compilation layer**:

- **`capability-contract.ts`**: declares canonical prompt fragments and tool descriptors for all capabilities (memory/widget/dashboard/tasks_and_notify/cli_tools/media_import/image_generation…)
- **`context-compiler.ts`**: pure function `compileContext(input)` — no IO, no tool exec, no permission decisions, only the compiled context
- **`mutation-level.ts`**: tool mutation-level classification (safe_read / safe_write / dangerous), the source of the permission allowlist
- **`builtin-event-bus.ts`**: tool-media-result side channel
- **`auto-invoke-accounting.ts`**: context accounting

**Why Harness is needed**: the three Runtimes need to show the model a consistent capability description. Harness guarantees Native/SDK/Codex system prompts do not drift.

### 5.3 Context compact

When context exceeds the model window, `context-compressor` triggers compact. On the SDK path, compact completes inside the SDK child; on the Native path, `pruneOldToolResults` trims between steps of `agent-loop.ts`.

---

## VI. The agent loop

### 6.1 Comparing the three Runtimes’ main loops

This is CodePilot’s most core chapter. The three Runtimes’ main-loop implementations are completely different:

#### SDK path (claude-client.ts)

`for await (const message of conversation)` at `claude-client.ts:2239`:

```typescript
// 这不是真循环，是事件泵
for await (const message of conversation) {
  switch (message.type) {
    case 'assistant': // 提取 tool_use → SSE
    case 'user':      // tool_result / 媒体块 / TodoWrite 同步
    case 'stream_event': // 文本 delta
    case 'system':    // init 元数据
    case 'result':    // extractTokenUsage + terminal_reason
  }
}
```

**Key points**:
- the real Agent loop is inside the SDK child (claude CLI)
- CodePilot only does event forwarding + guardrails
- guardrails include: two-level idle budget (10min before first token, 5.5min after), PTL compact retry, #577 result authority

#### Native path (agent-loop.ts)

`while (step < maxSteps)` at `agent-loop.ts:373` (default 50):

```typescript
while (step < maxSteps) {
  // 1. 清洗模型参数
  sanitizeClaudeModelOptions()
  buildAnthropicProviderOptions()
  
  // 2. 上下文裁剪
  pruneOldToolResults()
  
  // 3. 超时预算上膛
  timeoutCtl.onStepRequest()
  
  // 4. 单次 streamText()
  const result = streamText({ model, tools, messages, … })
  
  // 5. 逐事件翻译为 SSE
  for await (event of timeoutCtl.guardStream(result.fullStream)) {
    // text-delta→text, reasoning-delta→thinking,
    // tool-call→tool_use, tool-result→tool_result, …
  }
  
  // 6. 终止判断
  if (!hasToolCalls) break
  if (doomLoopDetected) break
  
  // 7. 进入下一轮
  messages += response.messages
  step++
}
```

**Key points**:
- the manual loop exists so permission checks, DB persist, timeout budgets, context trim can be inserted between steps
- these cannot hook the AI SDK’s automatic mode
- doom-loop detection: consecutive calls of the same tool

#### Codex path (codex/runtime.ts)

`codex/runtime.ts`:
- each conversation `thread/resume|thread/start`
- subscribe to notifications
- `turn/start`
- `event-mapper.translateCodexNotification` translates into canonical events then SSE

### 6.2 Essential differences of the main loop

| Dimension | Claude Code | CodePilot SDK | CodePilot Native | CodePilot Codex |
|---|---|---|---|---|
| Loop location | inside CLI | inside SDK child | agent-loop.ts while | inside Codex |
| Loop drive | first-party | SDK query() | manual while + streamText | Codex turn |
| Event forwarding | none (terminal direct) | for-await event pump | for-await translate | event-mapper |
| Guardrails | none | idle budget + PTL retry | timeout budget + doom detect | none |
| Context compact | internal | inside SDK | pruneOldToolResults | inside Codex |

### 6.3 Stream management

`stream-session-manager.ts` (1439 lines) manages SSE stream lifecycle:

- **`startStream`**: create the stream, `fetch('/api/chat')`, `consumeSSEStream`
- **Two-level idle budget** (#635): 10min before first token, 5.5min after, `setInterval` detects timeout then abort
- **Complete**: `buildFinalMessageContent` → phase=completed → `scheduleGC` (reclaim after 5min)
- **Error branches**: idle timeout / tool timeout auto-retry / user stop / general error
- **Stop**: `stopStreamWith` first arms a 2s force-abort safety net, then calls `/api/chat/interrupt`

### 6.4 Conversation registry

`conversation-registry.ts` (70 lines) manages active SDK sessions:

- globalThis `__activeConversationsV2__` (V2 key so HMR does not get an old-shape Map)
- `Map<sessionId, {query, lockId, abortController}>`
- **`unregisterConversation` gated by lockId** — only delete if lockId matches, preventing a superseded old turn’s late teardown from evicting the Query registered by the new turn (I1 race)

---

## VII. Tools and permissions

### 7.1 Two layers of tool sources

CodePilot’s tool system is two layers:

#### Coding-core tools (tools/)

8 tools: Read / Write / Edit / Bash / Glob / Grep / Skill / Agent

Each is a factory `createXxxTool(ctx)`; `tools/index.ts:45 createBuiltinTools()` aggregates them into an AI SDK `ToolSet`:

```typescript
// 标准 Vercel AI SDK ToolSet
{
  name: {
    description: string,
    inputSchema: zodSchema,
    execute: async (input) => result
  }
}
```

Fed directly to `streamText({ tools })`.

#### Platform tools (builtin-tools/)

7 groups of platform tools:
- **notification**: notify / schedule_task / list / cancel / hatch_buddy
- **memory-search**: 3 memory-search tools
- **dashboard**: 5 Dashboard tools
- **media**: import + generate_image
- **widget-guidelines**: Widget guidelines
- **session-search**: session search
- **cli-tools**: 6 CLI tools
- **ask-user-question**: user Q&A

`builtin-tools/index.ts:134 getBuiltinTools()` mounts by three gating conditions `always / workspace / keywords`, generating corresponding system-prompt fragments via `harness/runtime-adapter.ts adaptForNative()`.

### 7.2 Tool assembly

`agent-tools.ts:114 assembleTools()`:

```typescript
const tools = {
  ...createBuiltinTools(ctx),        // 编码工具
  ...getBuiltinTools(ctx),           // 平台工具
  ...buildMcpToolSet(mcpServers),    // 外部 MCP 工具
}

// 权限包裹
if (permissionContext) {
  tools = wrapWithPermissions(tools, permissionContext)
}
```

**plan mode** keeps only `PERMISSION_SAFE_TOOLS` (`agent-tools.ts:53`, derived from `harness/mutation-level.ts` `mutationLevel==='safe_read'`, fail-safe: undeclared tools default ask).

### 7.3 The essential difference vs Claude Code’s tool system

| Dimension | Claude Code | CodePilot |
|---|---|---|
| Tool format | hardcoded in the bundle | standard AI SDK ToolSet |
| Tool registration | config-style permission rules | zod schema + execute function |
| Tool composition | fixed | freely composable, gating, runtime wrap |
| Platform tools | none | builtin-tools (notify/memory/Dashboard/media…) |
| MCP integration | yes | yes (mcp-tool-adapter.buildMcpToolSet) |

### 7.4 Three-layer permission system

#### Layer 1: rule engine

`permission-checker.ts:127 checkPermission`:

- three modes: **explore** (read-only) / **normal** (writes pass, Bash ask) / **trust** (all pass)
- rule-array `findLast` semantics (specific rules override general)
- glob wildcard match on commands/paths
- hard floor: `DANGEROUS_PATTERNS` (rm -rf/sudo/kill/dd…) and `ALWAYS_ASK_TOOLS` (AskUserQuestion/ExitPlanMode) **must ask in any mode**

#### Layer 2: popup at execute time

`agent-tools.ts:236-341`:

- non-safe_read tools `checkPermission` before execute
- on `ask`, write DB `permission_requests`, send `permission_request` SSE (with HMAC `approvalToken` against forgery)
- `registerPendingPermission` hangs waiting for `/api/chat/permission` reply (5-minute timeout auto-deny)
- supports user-edited input (updatedInput) and “don’t ask this session” (sessionApprovals)

#### Layer 3: session Profile

`permission/profile.ts:23`:

- **default**: ask the user
- **auto_review**: hand to a restricted reviewer model for approval; human-only categories (credentials/billing/outbound/high-impact/interactive Q&A) never proxy-approve, fail-closed
- **full_access**: skip completely

`review-event.ts` + `review-audit.ts` unify proxy-approval audit events across the three Runtimes.

### 7.5 Sandbox

CodePilot itself has **no OS sandbox**:
- the Codex path depends on the sandbox that comes with codex app-server
- Native relies on the rule engine + human confirm
- that is the essential difference vs Claude Code (Seatbelt/Landlock/seccomp)

---

## VIII. Compaction and memory

### 8.1 Context compact

- **SDK path**: compact completes inside the SDK child
- **Native path**: `pruneOldToolResults` between `agent-loop.ts` steps
- **PTL compact retry**: `claude-client.ts:2859-3152`, `CONTEXT_TOO_LONG` triggers auto compact retry, `ptlRetryAttempted` against infinite loops

### 8.2 SQLite persist

`db.ts` (5752 lines):
- WAL mode + foreign-key constraints + busy_timeout=5000
- message content is a JSON array
- `sdk_session_id` used for SDK session resume

### 8.3 Session replay (rewind)

CodePilot supports **rewind to any checkpoint**:
- `file-checkpoint.createCheckpoint` creates snapshots at key nodes
- the user can roll back to a previous checkpoint and continue the conversation

### 8.4 Memory system

- **memory-search**: 3 memory-search tools in builtin-tools
- **Assistant Workspace**: Persona files, persistent memory, onboarding flow, daily check-ins

---

## IX. Sub-agents

### 9.1 Three spawn topologies

| Runtime | spawn method | Isolation level |
|---|---|---|
| CodePilot Runtime | independent `runAgentLoop` in the same Next/Electron server process | not an OS child |
| Claude Runtime | Agent SDK `query()` starts an independent OS child | isolated shadow HOME/credentials |
| Codex Runtime | reuse the app-server connection, new isolated `thread/start` | thread-level isolation |

### 9.2 Durable Lifecycle

The `subagent_runs` table implements a persistent lifecycle:

```
running → settling → completed/failed/partial/cancelled/timed_out
```

- write `subagent_runs.running` before starting the child (if write fails, fail-closed, do not start)
- after end `settling → terminal`; terminal is atomically closed only once
- user tasks aggregate by `logical_run_id`; each execution/retry is an incrementing attempt

### 9.3 Dependency orchestration

Declarative DAG via `workflow_id + task_key + depends_on`:

- downstream first `queued`, occupies no concurrency slot, does not call a Provider
- resolver polls the durable table waiting for upstream terminal (30-minute deadline, 5-second missing grace)
- upstream `result_text` is compiled into the downstream prompt tagged `<codepilot_dependency_results>` as “untrusted data”
- reject self-dependency / cycles / placeholder “wait” prompts

### 9.4 Constraints

- **depth fixed at 1**: Agent/spawn tools are hard-removed inside the child
- **concurrency 2 per parent session**
- **Claude child 5-minute idle + 30-minute hard cap**
- **parent abort propagates down**

### 9.5 Multi-model Sub-agent

`subagent-models.ts` generates an exact `providerId + modelId` route from all Provider resolvers + the runtime-compat matrix. The child’s `assembleTools` and `runAgentLoop` both switch to the target Provider.

---

## X. Ecosystem

### 10.1 MCP integration

- `mcp-tool-adapter.buildMcpToolSet`: aggregate tools from external MCP servers into the ToolSet
- supports stdio / sse / http three transports
- runtime monitoring

### 10.2 Skill system

- **format compatible with Claude Code**: YAML frontmatter + Markdown body
- **scan paths**: `.claude/skills`, `.claude/commands`, `~/.claude/skills`, `~/.claude/commands`, `~/.agents/skills`
- **marketplace**: install/uninstall API
- **fork mode**: lands in the subagent_runs durable system

### 10.3 Bridge subsystem

See Chapter 12, “Bridge and remote control.”

### 10.4 Codex integration

- **direct integration**: `codex/app-server-manager.ts:603 getCodexAppServer()` finds the `codex` binary and spawns `codex app-server` (stdio JSON-RPC, cached singleton)
- **codex/proxy**: local Responses API proxy (`/api/codex/proxy/v1/responses`), so the Codex CLI engine can run on any user-configured provider

---

## XI. Features

### 11.1 Provider management

`provider-catalog.ts` (2400 lines):
- `VENDOR_PRESETS` array defines 17+ vendor presets
- `VendorPreset` includes protocol, authStyle, baseUrl, defaultEnvOverrides, defaultModels, roleModels, billing model, sdkProxyOnly, claudeCodeVerified compatibility grading
- Zod-validates the whole table at startup

`provider-resolver.ts` (1793 lines):
- `resolveProvider()` priority `providerId > sessionProviderId > default_provider_id > active`
- virtual providers (`env`/`openai-oauth`/`xai-oauth`/`codex_account`)
- `resolveExactProvider()` fail-closed for background tasks

### 11.2 Task scheduling

- `tasks` table stores tasks
- `ensureSchedulerRunning` starts on the first chat request
- cron expressions or interval scheduling

### 11.3 Media generation

- `image-generator.ts`: Gemini/Anthropic image generation
- `job-executor.ts`: batch image-generation job executor
- Gallery: image gallery, tags, batch jobs

### 11.4 Error classification and diagnosis

`error-classifier.ts` (589 lines):
- 28 classes of structured error
- `ERROR_PATTERNS` per class defines patterns + errno codes + userMessage + actionHint + retryable
- `PROCESS_CRASH` containing session keywords is reclassified as `SESSION_STATE_ERROR`

`provider-doctor.ts` (1093 lines):
- 5 fast probes in parallel (CLI install/version, credentials/auth, default provider/model, feature compatibility, DNS/connectivity)
- 6th live probe: really spawn the CLI and send a minimal request
- repair actions: `REPAIR_ACTIONS` + `computeRepairs`

---

## XII. Bridge and remote control

### 12.1 Bridge architecture

`bridge-manager.ts` (1371 lines, globalThis singleton) orchestrates four modules:

| Module | Duty |
|---|---|
| `channel-adapter.ts` | `BaseChannelAdapter` abstract base + factory registry |
| `channel-router.ts` | message routing (IM → session) |
| `conversation-engine.ts` | consume SSE streams, save messages |
| `delivery-layer.ts` | outbound reliable delivery |

### 12.2 Channel adapters

5 adapters:
- **telegram**: long-poll + draft streaming preview
- **discord**: Discord.js
- **qq**: QQ Bot
- **weixin**: WeChat (auth/media/session-guard)
- **feishu**: Feishu (WS long connection + card streaming)

### 12.3 Permission forwarding

`permission-broker.ts`:

1. `full_access` profile and not a human-only tool → directly `resolvePendingPermission(allow)`
2. 30-second dedup window against duplicate cards
3. split by channel capability: button-less channels (QQ/WeChat) refuse AskUserQuestion
4. emit a card with `inlineButtons`, write `permission_link` table
5. user clicks a button → `handlePermissionCallback()` → `resolvePendingPermission()` unblocks the hanging stream

### 12.4 Markdown → IR → channel format

A three-stage pipeline:
1. `ir.ts` (829 lines): markdown-it token stream → MarkdownIR = `{text, styles[], links[]}`
2. `render.ts`: generic algorithm — split text at span boundaries, sort nested styles by `STYLE_ORDER`
3. `telegram.ts`/`discord.ts`/`feishu.ts`: only provide `styleMarkers + escapeText + buildLink` config

---

## XIII. Design philosophy and trade-offs

### 13.1 Why three Runtimes

CodePilot’s most core architectural decision is **Runtime is swappable**. Why?

- **Claude Code SDK**: best experience for Claude models, but locked to Anthropic
- **Native**: non-Claude models need their own loop, but cannot copy all of Claude Code’s capability
- **Codex**: OpenAI models need their own engine

The three Runtimes share frontend, DB, permissions, Bridge — but the main loops are completely different. A typical case of “product positioning deciding technical form.”

### 13.2 Why Electron + Next.js

- **Electron**: cross-platform desktop (macOS/Windows/Linux), system capabilities (pty/notifications/file dialogs)
- **Next.js**: App Router as both frontend and API layer, SSR/SSG, React 19 ecology
- **utilityProcess.fork**: Next.js server runs in an independent process; the main process is only the shell

### 13.3 Why SQLite

- **better-sqlite3**: sync API, WAL mode, zero config
- **local-first**: all data under `~/.codepilot/`, no cloud dependency
- **session replay**: SQLite persist supports rewind

### 13.4 Trade-off comparison vs Claude Code

| Dimension | Claude Code | CodePilot | Trade-off reason |
|---|---|---|---|
| UI | terminal TUI | Electron GUI | desktop users vs terminal users |
| Models | locked to Anthropic | 17+ providers | openness vs depth of optimization |
| Runtime | single | three, swappable | flexibility vs complexity |
| Tools | hardcoded | standard ToolSet | composability vs consistency |
| Permissions | five-gate adjudication | three layers | GUI popup vs terminal interaction |
| Sessions | terminal session | SQLite + rewind | persist vs lightness |
| Remote | none | IM Bridge | mobile scenes vs desktop scenes |
| Sub-agents | Task tool | durable lifecycle | reliability vs simplicity |

### 13.5 Three most distinctive designs

1. **Runtime is swappable**: the same frontend, three different Agent engines. The essential difference between CodePilot and every other agent client.

2. **Harness capability contract**: `harness/context-compiler.ts` is a pure-function compilation layer, guaranteeing the three Runtimes’ capability descriptions to the model do not drift. A delicate balance between “unified infrastructure” and “modules evolving independently.”

3. **Bridge permission forwarding**: `permission-broker.ts` turns permission requests into IM inline buttons, breaking the deadlock of “Claude stream blocked waiting for permission.” The key innovation of “how a desktop agent is remotely controlled.”

---

## Side-by-side: CodePilot’s place beyond the thirteen

> CodePilot is the **sixteenth project** analyzed in this repo. Its relation to the previous fifteen is not “another coding agent,” but a form jump of “moving the agent from the terminal onto the desktop.”

### Vs Claude Code (the most direct counterpart)

| Dimension | Claude Code | CodePilot | Essence of the difference |
|---|---|---|---|
| Product positioning | terminal coding agent | desktop general agent client | terminal vs desktop |
| UI | Ink TUI | Electron + React | terminal vs GUI |
| Models | locked to Anthropic | 17+ providers | closed vs open |
| Runtime | single first-party | three, swappable | unified vs flexible |
| Main loop | first-party while loop | SDK outsourced / Native first-party / Codex outsourced | autonomous vs mixed |
| Tools | 43 hardcoded | standard ToolSet 8+7 groups | private vs standard |
| Permissions | five-gate adjudication | three layers (rules+popup+Profile) | terminal vs GUI |
| Sessions | terminal session | SQLite + rewind | temporary vs durable |
| Remote | none | IM Bridge 5 channels | none vs yes |
| Sub-agents | Task tool | durable lifecycle | simple vs reliable |
| Sandbox | Seatbelt/Landlock/seccomp | none (Codex brings its own) | yes vs no |

### Vs open-design (both “host” forms)

| Dimension | open-design | CodePilot | Essence of the difference |
|---|---|---|---|
| Host object | 25 CLI agents | 3 Runtimes | count vs depth |
| Core innovation | adapter-as-data | Runtime swappable | breadth vs depth |
| Main loop | does not write (outsourced to CLI) | three (outsourced+first-party+outsourced) | pure host vs mixed |
| Content | design system/brand/templates | chat/tasks/media/remote | design vs general |
| Output | HTML/PPT/PDF | dialogue/code/images | documents vs interaction |
| Permissions | none (CLI brings its own) | three layers | none vs yes |

### Vs openworker (both “general-task colleagues”)

| Dimension | openworker | CodePilot | Essence of the difference |
|---|---|---|---|
| Positioning | unattended colleague | desktop agent client | background vs foreground |
| Interaction | Slack/email | desktop GUI + IM Bridge | async vs sync |
| Models | provider-agnostic | 17+ providers | similar |
| Tools | 40 connectors | 8+7 groups + MCP | connectors vs standard |
| Permissions | five-gear modes | three layers | similar |
| Persist | SQLite | SQLite | same |

### Vs Codex (the source of the third Runtime)

| Dimension | OpenAI Codex CLI | CodePilot Codex Runtime | Essence of the difference |
|---|---|---|---|
| Form | terminal CLI | embedded in desktop GUI | independent vs integrated |
| Models | locked to OpenAI | locked to OpenAI (but via proxy can run any provider) | closed vs half-open |
| Main loop | run_turn two-layer loop | inside Codex | same |
| Sandbox | three-platform OS sandbox | Codex brings its own | same |
| Permissions | AskForApproval × sandbox | reuse CodePilot three layers | independent vs reuse |

### CodePilot’s distinctive contributions

CodePilot brings three designs none of the previous fifteen have:

1. **Runtime is swappable**: the same frontend, three different Agent engines. This is not “multi-provider” (that is openness at the model layer), but “multi-engine” (openness at the loop layer).

2. **Harness capability contract**: `harness/context-compiler.ts` is a pure-function compilation layer, guaranteeing the three Runtimes’ capability descriptions to the model do not drift. A delicate balance between “unified infrastructure” and “modules evolving independently.”

3. **Bridge permission forwarding**: `permission-broker.ts` turns permission requests into IM inline buttons, breaking the deadlock of “Claude stream blocked waiting for permission.” The key innovation of “how a desktop agent is remotely controlled.”

---

## Appendix: key-file index

| File | Lines | Notes |
|---|---|---|
| `src/lib/claude-client.ts` | 3628 | SDK wrap + message stream |
| `src/lib/agent-loop.ts` | 1019 | Native loop |
| `src/lib/db.ts` | 5752 | SQLite schema + migrations |
| `src/lib/stream-session-manager.ts` | 1439 | SSE stream management |
| `src/lib/conversation-registry.ts` | 70 | session registry |
| `src/lib/error-classifier.ts` | 589 | error classification |
| `src/lib/provider-doctor.ts` | 1093 | Provider diagnosis |
| `src/lib/runtime/registry.ts` | 198 (`resolveRuntime` @72-152) | Runtime registry |
| `src/lib/tools/index.ts` | 67 (`createBuiltinTools` @45) | tool aggregation |
| `src/lib/builtin-tools/index.ts` | 351 (`getBuiltinTools` @134) | platform tools |
| `src/lib/permission-checker.ts` | (`checkPermission` @127; file total lines per the repo) | permission rule engine |
| `src/lib/bridge/bridge-manager.ts` | 1371 | Bridge manager |
| `src/lib/harness/context-compiler.ts` | — | context compilation |
| `src/lib/codex/app-server-manager.ts` | 769 (`getCodexAppServer` @603) | Codex integration |
| `src/lib/provider-catalog.ts` | 2400 | Provider presets |
| `src/lib/provider-resolver.ts` | 1793 | Provider resolve |
| `electron/main.ts` | 2566 | Electron main process |
| `src/app/api/chat/route.ts` | 990 | chat API |

---

*This lecture was written from a file-by-file analysis of CodePilot v0.62.0 source. Every `file:line` citation was re-checked with `grep -n` / `sed -n`.*
