OPEN SOURCE DEEP DIVE
Octop: a self-hosted multi-user, multi-agent assistant in one Python process
Tencent Cloud's open-source, self-hosted AI assistant platform (MIT, 8,777 stars). One process serves the FastAPI backend, React dashboard, CLI, IM channels and cron, with a SQLite WAL control plane and optional PostgreSQL. It composes four runtime libraries (octop-harness, octop-gateway, octop-memory, octop-browser) into a single self-hostable product, adding an expert library with AgentTeams, bidirectional ACP, a cross-instance Bridge, tool guardrails and PII redaction, with ADRs and line-level specs as reviewable evidence.
What it is
Octop is Tencent Cloud's open-source, self-hosted AI assistant platform, MIT licensed, created in July 2026 and currently at version 1.0.2b6 (CHANGELOG dated 2026-10-04). Its own description is a multi-user, multi-agent self-hosted assistant, but what separates it from the usual chat-wrapper crowd is the delivery shape: one Python wheel containing a FastAPI backend, a React dashboard, a Click CLI, IM channel ingestion and cron scheduling. octop run brings all of it up, and every byte of state lives under ~/.octop/. No external queue, no required backing service beyond whatever LLM provider you configure.
At collection time the repository had 8,777 stars and 1,075 forks. The backend is Python 3.12+; the frontend is React 18 with TypeScript, Vite and Ant Design. Native desktop clients ship for Windows, macOS and Linux alongside FnOS NAS packages, which tells you a lot about the intended deployment: one machine at home or in the office. The dashboard sidebar lists chat, experts, tasks, connectors, skills, token stats, channels, subagents, terminal AI+, browser AI+, remote desktop, ACP, MBTI personas and memory, and that navigation doubles as a feature inventory.
Why it belongs in the harness section: the host layer above the harness
Octop is not itself a harness. It composes four separately versioned runtime libraries into one process: octop-harness (the LangGraph-based agent runtime owning model routing, tools, skills and conversation checkpoints), octop-gateway (normalizes Feishu, DingTalk, QQ, WeChat, Telegram, Discord and WeCom messages into a single processing pipeline), octop-memory (hierarchical recall plus full-text search, so memory travels with the workspace) and octop-browser (CDP-based browser automation with persistent profiles). The CHANGELOG shows 1.0.2b3 (2026-09-26) aligning all four runtimes on their 1.0.0 releases, and 1.0.2b5 (2026-09-29) adding the Octop-to-Octop Bridge. That version line is itself a record of how a host and its runtimes release in lockstep.
So the reason it sits in the harness section differs from an inference-kernel project: Octop is a complete specimen of the layer above the harness. Any self-hosted product that packs multiple harness instances into one process must answer where session keys are encoded, where checkpoints live, who owns the memory database, how an IM message and a web chat share one runtime, and how the whole runtime tree is rebuilt from the database after a crash. Octop answers all of these in code, and writes the reasoning down as ADRs.
One process, no queue: the ledger in ADR 001
ADR 001 (2026-06, status Accepted) states the first-principles decision plainly: the web server, CLI, per-user agent runtimes, IM connections and cron schedulers all run inside a single uvicorn process. No Redis, no RabbitMQ, no Celery, no separate worker. Four reasons are given: the self-hosted install path is pip install octop && octop init, and adding Redis doubles the ops burden for the primary audience; agent calls are LLM-bound I/O waits, which asyncio fans out efficiently; concurrent writes are rare (one writer per agent at a time), so SQLite WAL is enough; and restart semantics stay simple because OctopServer.start() rebuilds the entire runtime tree from the SQLite file with no external state to reconcile.
The value of this ADR is that it also books the costs: vertical scaling only (one machine), heavy CPU tasks block the event loop, no horizontal worker scaling. It even names where a future split would start: the seam is already partially visible in infra/gateway/processor.py. Writing down what you are not doing today and where you would cut tomorrow carries more information than a prettier architecture diagram.
Three storage layers, do not conflate: ADR 002
ADR 002 (2026-07-20) opens with a "do not conflate" warning, because Octop carries three storage layers at once and conflating them is the classic failure mode for this class of project. The control plane holds users, agents, channels, cron, settings and secrets. Harness checkpoints hold conversation graph state. Agent memory belongs to octop-memory. Workspace content files (SOUL, skills, inbound, session JSONL) remain files behind BackendWorkspace, separate from the control-plane database entirely.
| Layer | Contents | Default | Optional |
|---|---|---|---|
| Layer 1 control plane | users / agents / channels / cron / settings / secrets | SQLite (WAL) under ~/.octop/ | PostgreSQL (psycopg3 pool, 1 to 8) |
| Layer 2 harness checkpoints | conversation graph state | {workspace}/checkpoints.sqlite | reuse octop-harness PostgresSaver when memory is postgres |
| Layer 3 agent memory | octop-memory hierarchical recall and FTS | SQLite control plane: {workspace}/memory.sqlite; PG control plane: same DSN, schema agent_<id> | explicit config_json.memory.backend override |
| Workspace content files | SOUL, skills, inbound, session JSONL | files via BackendWorkspace | local disk / Docker sandbox / PostgreSQL / COS or S3 |
Several implementation constraints are worth stealing. Repos keep ? placeholders and PostgresPool rewrites them to %s at the boundary. SQLite and PostgreSQL each get their own numbered migrations (NNN_*.sql and NNN_*.pg.sql). Control-plane pg migrations must not run CREATE EXTENSION, because many managed Postgres roles cannot create extensions and the control plane does not need vector anyway (octop-memory uses built-in tsvector). There is a single active writer with no multi-instance write promise. The PG path is greenfield only: no SQLite-to-PostgreSQL migrator. Backup manifests carry database_driver and database_dump_format, and cross-engine restore is refused. The common thread is a willingness to cut features rather than leave a door that looks fine and fails at 3am.
Experts, personas and AgentTeams: a team is a special kind of expert
The base unit in Octop is not an agent but an expert. Each user can own several experts, each with its own workspace, providers, channels and cron. Sixteen MBTI persona templates plus a custom system prompt ship in the box. The expert library is scanned at boot from infra/agents/experts/library/, and there is an expert market plus in-deployment sharing. The dashboard's expert page renders the library as template cards: general assistant, primary-care physician, system doctor, cluster doctor, trending-news reporter, office automation, ops engineer, parenting steward, securities observer, each marked installed or not, each offering "create from this template".
AgentTeams (Beta) extends this model, and its identity design is notably restrained: a team is not a parallel entity, it is a special expert with agents.kind = team. The room id is the coordinator's thread_id; a member's checkpoint is written as "main thread ~ member id"; the group timeline is relayed by the gateway with a speaker_agent_id per bubble. Members write to their own workspaces; team rules and collective memory live only on the coordinator side and are injected into the task message at dispatch time. Channels bind to the coordinator only. The design doc lists 23 confirmed decisions, several of which pin implementation boundaries: a member with in-flight work cannot be removed from the team and its expert cannot be deleted; a member may synchronously ask_agent a colleague but may not asynchronously drag another agent into the room; stopping halts only the coordinator's current turn while members finish and call back; and every new user message goes to the coordinator first, even mid-member-speech.
| Expert (kind=expert) | Team coordinator (kind=team) | |
|---|---|---|
| Workspace / checkpoint / memory | yes | yes (team memory root) |
| System prompt | expert template | hidden dispatch template plus a runtime "digest then rewrite" constraint |
| Tools | the full working set | lightweight + agent_list + async ask_agent |
| Visible peers | the user's other experts by default | only team_peers, i.e. team members |
| Channels | bindable (1:1) | bindable (team entry point) |
The decisive row is the coordinator's narrowed toolset: agent_list, async ask_agent, memory and time. No filesystem, no browser, no search, no MCP, no skills, no plugins, and no skill, subagent, plugin or persona options at create or edit time. The coordinator schedules and does not do. That single constraint keeps the most common multi-agent failure mode, the orchestrator writing files itself, outside the tool table.
Four surfaces, one runtime
Web, CLI, IM and cron share the same harness runtime; only the session key encoding differs. Web uses <aid>:dashboard:<user_id>:dm, CLI uses <aid>:cli:<user_id>:dm, IM uses <aid>:<channel_kind>:<platform_session>:<dm|group>, and cron uses the session_key column on the job row (defaulting to the owner's dashboard key). The thread_id itself encodes no origin; origin is a column on chat_sessions and threads. Thread reuse and archival are row updates, with the state machine in infra/gateway/threads.py. Keys as convention, origin as data: the same conversation is attributed correctly across surfaces instead of being guessed from string parsing.
Streaming chat moved from SSE to WebSocket (/api/agents/{aid}/chat/ws) because the dashboard needs to send slash commands and /compact mid-turn. The one route still on SSE is POST /api/agents/{aid}/chat/hitl/resume, which only needs to stream a resumed turn back to the client. The channels page shows the granularity of IM ingestion: WeChat, QQ, WeCom, Feishu, Yuanbao, DingTalk, Telegram, Xiaoyi (OpenClaw over A2A) and MQTT, each an individual toggle, scoped per currently selected expert.
Bidirectional ACP and the cross-instance Bridge
ACP (Agent Client Protocol) runs in both directions. Inbound: octop acp --agent main starts a stdio ACP server so editors such as Zed or OpenCode can drive your Octop expert. Outbound: Octop delegates to external coding agents. Built-in runners include OpenCode, CodeBuddy, Claude Code and Codex, with kimi_code, cursor_cli and pi also listed in configuration; runners are configured per user under /acp in the dashboard, then the acp_runner tool is enabled per agent so chat can delegate behind a permission gate.
The Bridge (Octop-to-Octop instance bridge) solves a different problem: a user-level connection to another Octop, cloud or a second local box, letting your dashboard list the remote expert catalog, pull conversation history on demand and talk to remote experts as if they were local, symmetrically. The design document draws hard borders: conversation executes only on the instance that owns the expert and the initiating side does not persist full history; remote agent ids look like bridge:{connection_id}:{remote_agent_id}; the browser talks only to the local API and the local side forwards over the bridge; transport is a federated long-lived WebSocket carrying a generic HTTP tunnel with turn-level stream multiplexing. Explicit non-goals include reworking ask_agent into a cross-instance protocol, double-writing full chat logs on both sides, and letting the browser hit the remote baseURL directly. The doc marks itself as in implementation with the Phase 0 to 3 skeleton landed, so read it as work in progress.
Engineering discipline: the bans are in the repo
The root AGENTS.md is a navigation guide for coding agents, but its module boundary table is equally useful to humans: dependencies flow inward, transport layers call the domain and the domain never calls back into HTTP or CLI; infra/db/repos/ keeps one repo per table with SQL only; infra/utils/ is pure helpers that must not import any other infra package; api/routers/ must stay thin, validating HTTP, calling infra and mapping errors without inventing domain rules. Six hard bans are listed, including infra/ never importing api/, cli/ or launch.py, and exactly one module, launch.py, allowed to import both infra/server and api/app. The ship bar is make all (format, lint, typecheck, test) green; tests run via uv run pytest, never bare pytest; CI runs on Linux and Windows, so tests may not assume a single platform.
The scale is no longer toy-grade. In the clone we collected, infra/ holds 488 Python files and 106,895 lines, api/ 105 files and 29,877 lines, cli/ 46 files and 5,579 lines, dashboard/src 934 ts and tsx files and 181,772 lines, and tests/ 501 files and 90,411 lines. Test code at roughly ninety percent of the infra line count is an unusual ratio among self-hosted projects of this kind.
| Directory | Files | Lines |
|---|---|---|
src/octop/infra | 488 .py | 106,895 |
src/octop/api | 105 .py | 29,877 |
src/octop/cli | 46 .py | 5,579 |
dashboard/src | 934 .ts / .tsx | 181,772 |
tests | 501 files | 90,411 |
The clearest evidence of discipline is a spec document. docs/octop-ui-payload-offload.md tackles a concrete bug: the bundled bilibili plugin returns 13 to 24 KB of JSON from a single tool call (the PoC reproduces 22.5 KB for a 200-episode series), and that one string serves four consumers with contradictory needs. The model context wants it small; history persistence wants it complete; the dashboard's live frames want it complete; the plugin UI wants it complete. Truncation is not an option because the UI would lose episodes. The chosen fix separates the data plane (the large payload, carried as an artifact) from the control plane (summary plus render hint) after tool execution but before the ToolMessage enters LangGraph state: the artifact never reaches model context, yet survives in WS frames, persisted history and the plugin UI. The spec is marked v2, reviewed independently by three models that returned APPROVE WITH CHANGES, and it lists twenty verified facts, each with source file and line numbers, backed by a PoC script driving a real LangGraph agent loop. Treating model review plus line-level evidence plus runtime reproduction as the admission bar for a spec is the most portable idea in this repository.
| Consumer | Need | Path |
|---|---|---|
| LLM context | as small as possible; ToolMessage.content enters state and is carried in full on every later model call | LangGraph state |
| History persistence | complete data for replay | thread_messages (recorder main path message_to_dict) |
| Dashboard live rendering | complete data | WS tool_result frames (model_dump) |
| Plugin UI | complete data | props.data parsed from tool output |
Operational restraint: memory slim and history_v2
octop memory slim maintains the memory database online: it connects to the same running OCTOP_HOME instance instead of starting a second Octop, waits for the current task to finish (failing out after 120 seconds without an idle window rather than migrating mid-turn), then backs up, deduplicates historical context, compacts and restores. Each database gets its own backup named memory.sqlite.before-slim.<random id>.bak, never auto-deleted or overwritten. Any failure stops the batch immediately, reporting completed and skipped counts with no automatic retry. The documentation makes an unusual promise: backup and VACUUM stages never report fake percentages or remaining time, and growing elapsed time only proves the connection is still responsive. During maintenance the memory panel's RPC returns AGENT_BUSY instead of letting synchronous SQLite lock waits block status polling, and agents on other databases keep working.
Segmented history archival (history_v2) is equally restrained: off by default, no historical data migration, no checkpoint cleanup, and switching happens only at complete-turn boundaries. The new history_v2.sqlite stores message or trajectory structures in documents and content-hashed bodies (visible thinking, tool arguments and results) in bodies, so an identical large field can be referenced by both views instead of storing two full copies of the chat. The feature supports only a SQLite main database and a single service process; enabling it on PostgreSQL refuses to start. The document also declines to promise that project history collapses to one copy, and existing SQLite files do not shrink immediately. A feature that defaults off and refuses unsupported backends is more reassuring than a migration script that defaults on.
Deployment shapes and security defaults
Installation is a one-line script (uv provisions an isolated Python 3.12 under ~/.octop/) or pip install octop. octop init interactively creates the database, JWT secret and first admin; octop run serves in the foreground on port 8088 by default, with --host and --port overrides; octop service start registers a systemd, launchd or Windows service. On Docker first boot without OCTOP_DEFAULT_PASSWORD, a random admin password is generated and written to /data/.octop/credential.txt; the password policy requires at least eight characters with letters and digits, and weak choices are rejected by the app policy and replaced with a random one. Security defaults include JWT multi-user auth with row-level isolation (agent rows matched against the caller's user_id, admin bypass allowed), PII redaction before data leaves the workspace, explicit approval for risky tools and shell commands under user-editable rules in ~/.octop/security/tool_guard/, and workspace files kept separate from the control-plane database with pluggable backends: local disk, Docker sandbox, PostgreSQL, COS or S3.
The Browser AI+ page embeds a controlled headless Chromium session directly in the dashboard, complete with tabs, an address bar and a skill-recording button; the remote desktop page captures and injects input into the host desktop session on Linux, Windows and macOS, with one-click isolated desktops on headless Linux. These two surfaces extend the agent's reach from text into GUIs, and they are the most visible difference between Octop and a pure chat assistant.
Limits you should judge for yourself
Several caveats belong on the record. First, AgentTeams is Beta, the Bridge is marked in implementation, and history_v2 is off by default and SQLite-only: these three are directions, not finished goods, and evaluating them against the README highlight table will mislead you. Second, the ceiling of the single-process model is written into the project's own ADR: one machine, vertical scaling only, heavy CPU work blocking the event loop, and no multi-instance write promise, which rules out using Octop as a horizontally scaled service backbone. Third, the expert market, skill sharing and connector ecosystem bring third-party content into the same runtime; tool approval and shell guardrails mitigate that, they do not eliminate it. Fourth, we have not benchmarked it or run it long-term: 8,777 stars measure attention, not capability. Octop earns its place in the harness section because it turns "how does a host layer orchestrate many harness instances" into reviewable documents and code: ADRs for trade-offs, AGENTS.md for bans, specs for line-level evidence. For anyone building their own agent platform, that documentation is a reusable asset in itself.