English lecture · source-traced

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.

EN
lecture
source
file:line
no
embellishment
中文
sibling

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.


Part 1

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

Part 2

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

Part 3

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
  3. listen on a free port (serverProcess)
  4. GET /api/bridge health check
  5. SIGTERM → 3s → SIGKILL shutdown
  6. mainWindow.loadURL(http://127.0.0.1:${port})
  7. TerminalManager (pty) init
  8. Tray / notifications / file dialogs registered
  9. 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:

-- 核心表
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.


Part 4

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:
  8. SDK: streamClaudeSdk()
  9. Native: runAgentLoop()
  10. Codex: codex app-server
  11. SSE stream return
  12. Background persist: collectStreamResponse (continues even if the renderer disconnects)
  13. 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


Part 5

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.


Part 6

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:

// 这不是真循环,是事件泵
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):

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)

Part 7

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:

// 标准 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():

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)


Part 8

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

Part 9

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.


Part 10

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

Part 11

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


Part 12

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


Part 13

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.”


Part 14

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.”


Part 15

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.

↑