English lecture · source-traced

Suna / Kortix source analysis: an AI command center that runs a company as a Git repository

**Subject**: [kortix-ai/suna](https://github.com/kortix-ai/suna) (product name **Kortix**; the repo name is a historical leftover still called Suna) **Based on commit**: `5862e95671cfd198c6a94ceb9c1fefe579f00a8b` (full-checkout deepened edition; `git rev-parse HEAD`) **Version**: root `VERSION` is `0.10.14`; the README badge still says `0.9.98` (`README.md:12`) — **treat the `VERSION` file as canonical** **Analysis d

EN
lecture
source
file:line
no
embellishment
中文
sibling

Subject: kortix-ai/suna (product name Kortix; the repo name is a historical leftover still called Suna) Based on commit: 5862e95671cfd198c6a94ceb9c1fefe579f00a8b (full-checkout deepened edition; git rev-parse HEAD) Version: root VERSION is 0.10.14; the README badge still says 0.9.98 (README.md:12) — treat the VERSION file as canonical Analysis date: 2026-07-24 Document version: full-checkout deepened edition (relative to the old sparse edition, this substantially thickens apps/web, the full change-request path, snapshot/providers, and packages/{manifest-schema,starter,registry,executor-sdk}; every claim can be traced to file:line) Audience: beginners with a little programming. Each chapter tries to follow “metaphor → real source → critical-path explanation → design trade-offs.” Note: this lecture uses the same 12-chapter frame as the sibling analyses of Claude Code / opencode / hermes-agent / Raven / CodeWhale / nanobot / goose / OpenManus, so you can compare them sideways. All file:line citations are relative to 参考项目/suna/. This workspace is a full tree (including apps/web, desktop-electron, mobile, and every packages/*). Features that cannot be found, or that we are not sure about, are explicitly marked not found — never invented. Product axis: Kortix aims at a “company operating system / general-task Agent platform” (research, deliverables, automation, multi-channel kickoff). It is not a Claude Code–style “write code in this repo first” single-machine coding agent. It treats OpenCode as the brain inside the sandbox, and itself owns isolation, permissions, connectors, billing, and change requests.


Part 1

Chapter 1 · Project overview: what it is, who builds it

One-sentence version

Kortix (GitHub repo kortix-ai/suna) is an AI Command Center that takes “the company” literally and makes it a Git repository: configuration, agents, skills, memory, and sandbox specs all live in the repo; each session boots an isolated sandbox that runs OpenCode; only changes that have passed a change request review can merge back to main. The license is Elastic License 2.0 (LICENSE:1), not MIT — it explicitly restricts offering this software as a hosted service to third parties (LICENSE:18-20).

The README title is the product name (README.md opens with # Kortix); site metadata pins the positioning as “open-source AI command center for your company” (apps/web/src/lib/site-metadata.ts:16). The MANIFESTO’s first sentence is even blunter (MANIFESTO.md:3):

A company is going to be a git repository.

Understand the positioning with a metaphor

If Claude Code is a “fine-dining sushi bar” (the menu is built around writing code), and OpenManus is a “street-stall research travel agency” (single-machine ReAct + a browser), then Kortix is more like a gated tech park plus a self-serve workstation system:

  • The blueprint of the company building is a Git repo (kortix.yaml + .kortix/opencode/);
  • Each time you start work, you get an independent workstation (a cloud sandbox) with the toolchain and OpenCode already installed;
  • If you want to keep the work, you can only open a CR (change request) for human review — you cannot graffiti the shared lobby wall;
  • The front desk can be Web / CLI / Desktop / Mobile / Slack / Teams / Email — every utterance still enters the same session lifecycle.

The MANIFESTO states the other half of the bet in the open (MANIFESTO.md:7-9): the AGI era needs a “WordPress / command center” — context, agents, skills, triggers, connectors, and memory in one place, not forty tools taped together.

Who builds it / what it is called

Item Evidence
Org / repo kortix-ai/suna (README / repo URL)
Product name Kortix (README title, CLI kortix, package names @kortix/*)
Historical name Suna repo name, suna-migration/, some comments still keep it
License Elastic 2.0 (LICENSE:1-20)
Version VERSION → 0.10.14; README badge still 0.9.98 (README.md:12)
Language main repo TypeScript; runtime Bun (API / daemon); pnpm workspace (package.json:60; pnpm-workspace.yaml:1-3)
Web package name Kortix-Computer-Frontend, Next 15.5.21 (apps/web/package.json:2,159)
Desktop Electron shell package @kortix/desktop-electron, wrapping Web (apps/desktop-electron/package.json:2-7)
Mobile Expo app kortix (apps/mobile/package.json:2-4)

Stack at a glance

Layer Tech Location / evidence
Frontend host Next.js 15 App Router (Turbopack in development) apps/web/package.json:6,159; AGENTS.md:179
Client SDK @kortix/sdk (SSoT; hosts must not talk to the API/OpenCode directly) AGENTS.md:91-137; packages/sdk/src/index.ts:1-39
API Bun + Hono OpenAPI, default :8008/v1 AGENTS.md:180; apps/api/src/index.ts
Data Supabase + Drizzle (@kortix/db) AGENTS.md:181; packages/db/
Manifest contract @kortix/manifest-schema (kortix.yaml validation SSoT) packages/manifest-schema/package.json:2-4
Scaffolding @kortix/starter (init / create-repo templates) packages/starter/package.json:2-4
Marketplace engine @kortix/registry (shadcn-compatible registry) packages/registry/package.json:2-4
Connector client @kortix/executor-sdk (server-side proxy send) packages/executor-sdk/package.json:2-5
Sandbox providers Daytona / Platinum / E2B / local-docker provider-parity.test.ts:11-34
Brain inside the sandbox OpenCode (opencode serve) supervised by kortix-agent apps/sandbox/README.md:15-19; kortix-sandbox-agent-server/README.md:6-12
Model gateway apps/llm-gateway + API-side routing apps/api/src/llm-gateway/; index.ts:740-741
Connector execution /v1/executor, credentials stay on the server apps/api/src/executor/execute.ts:1-5
flowchart LR
    subgraph Hosts["Host surface"]
      WEB["apps/web Next.js"]
      DESK["desktop-electron"]
      MOB["mobile Expo"]
      CLI["kortix CLI"]
      CH["Slack / Teams / Email / …"]
    end
    SDK["@kortix/sdk"]
    API["apps/api :8008"]
    DB[(Supabase / @kortix/db)]
    SBX["Cloud sandbox
session_id == sandbox_id"] DAEMON["kortix-agent"] OC["opencode serve"] CR["change_requests"] WEB --> SDK DESK --> WEB MOB --> SDK CLI --> API CH --> API SDK --> API API --> DB API -->|"/v1/p/:id/8000"| SBX SBX --> DAEMON --> OC OC -->|"commit + kortix cr"| CR CR --> API

Axis differences versus other projects in this series (plant the target first)

  • vs Claude Code / opencode on a single machine: Kortix embeds OpenCode inside a multi-tenant sandbox OS, and itself owns identity, billing, connectors, and CRs.
  • vs OpenManus: OpenManus is an in-process ReAct inheritance chain; Kortix’s “think one step, do one step” lives in the OpenCode binary. This repo writes the platform spine. See the short note at the end of this chapter and Chapter 12.
  • vs nanobot / Raven: nanobot is a personal IM butler + a small core; Kortix is an enterprise command center + Elastic license + sandbox isolation.

Contrast with OpenManus (short note)

Same axis (general tasks / research / deliverables), different bets: OpenManus bets on “a ReAct inheritance chain you can understand in three hours”; Kortix bets on “the company repo + one session, one sandbox + change request.” On license, OpenManus is MIT; Kortix is Elastic License 2.0. Details are in the sibling lecture OpenManus-vs-Suna side-by-side comparison.


Part 2

Chapter 2 · Panorama architecture: a layered map

Metaphor: six floors of the tech park

  1. Gate and reception (Hosts + SDK): Web/CLI/IM/Mobile enter the park only through @kortix/sdk or the official CLI (AGENTS.md:110-118).
  2. Dispatch center (apps/api): session lifecycle, project Git, billing, IAM, channel webhooks, sandbox proxy.
  3. Tool broker (executor + llm-gateway): third-party APIs and model traffic are sent from the server; the sandbox holds only a scoped token.
  4. Workstation (sandbox + kortix-agent): clone the repo, switch to the session branch, bring up OpenCode, expose 8000 / health checks / static preview.
  5. Archives (git + change_requests + DB mirror): truth lives in the sandbox and git; the DB is for list/sync acceleration (MANIFESTO.md:50).
  6. Contract layer (packages): manifest-schema / starter / registry / sdk / db / executor-sdk — so CLI, API, and Web share one set of shapes.

Directory map (full checkout)

Path Responsibility
apps/web/ Next.js Command Center UI; thin consumer of SDK
apps/api/ multi-tenant control plane; src/index.ts mounts almost every /v1/*
apps/cli/ kortix init/ship/sessions/cr/…
apps/sandbox/ sandbox Docker reference image and entrypoint
apps/kortix-sandbox-agent-server/ in-sandbox daemon source (compiled into the image)
apps/llm-gateway/ gateway service implementation (API also has routing hooks)
apps/desktop-electron/ Electron shell, loads the Web URL
apps/mobile/ Expo mobile client
packages/sdk/ the host’s only data layer
packages/db/ Drizzle schema (including change_requests, session_sandboxes)
packages/shared/ cross-package constants/utils
packages/manifest-schema/ kortix.yaml spec and validator
packages/starter/ project templates (init / create-repo)
packages/registry/ Marketplace discovery/build primitives
packages/executor-sdk/ connector-call client
packages/agent-tunnel/ device tunnel
packages/api-contract/ Zod wire contracts
packages/llm-catalog/ model catalog

What the Web-side “Command Center” actually looks like

Product copy repeatedly calls it a command center (apps/web/src/lib/site-metadata.ts:16; marketing hero apps/web/src/features/marketing/hero.tsx:46). The real in-app operating surface is the project workspace under the App Router:

Route File Role
/projects/[id] apps/web/src/app/(app)/projects/[id]/page.tsx project home: ProjectHome composer opens a new session
/projects/[id]/sessions/[sessionId] …/sessions/[sessionId]/page.tsx the session page that mounts useSession
/projects/[id]/customize/… customize pages Agents / Secrets / Sandbox / Review, etc.
/review apps/web/src/app/(app)/review/page.tsx Review Center prototype (mock, no API)
In-project Review features/workspace/…/review-view.tsx real data wired through ReviewCenterConnected

features/ is cut by domain: session/ (chat and tool rendering), workspace/ (sidebar/home), review-center/, project-files/, marketplace/, providers/ (Auth / LLM provider UI).

API route spine (one table for the control plane)

apps/api/src/index.ts hangs subsystems off a unified Hono app (excerpt, index.ts:693-851):

  • /v1/accounts, /v1/auth, /scim/v2 — identity and enterprise directory
  • /v1/router, /v1/generation, /v1/usage — model routing and usage
  • /v1/billing — Stripe and related billing
  • /v1/platform — sandbox platform capabilities
  • /v1/projects — Git-backed projects and sessions (including change-requests)
  • /v1/marketplace — Agent/skill marketplace
  • /v1/git — git-proxy (conditionally mounted)
  • /v1/executor — connector execution
  • /v1/webhooks/* — Slack/Teams/Telegram/Email/Meet/sandbox lifecycle
  • /v1/tunnel — device tunnel
  • /v1/p — sandbox preview/proxy (the external entry for OpenCode SSE/HTTP)

The process-level crash guard is deliberately “log only, do not take down the whole machine” (index.ts:98-119) — a typical trade-off for a long-lived multi-tenant service.

Two “main loops” you must separate first (the key)

Loop Lives where Does what
Platform session loop apps/api/.../session-lifecycle/ create → provision sandbox → start/ready → continue/deliver prompt → stop/restart; queues, idempotency, backpressure
Model–tool loop OpenCode (binary inside the sandbox) classic Agent: read context → call tools → observe → think again; this repo does not rewrite that chain

The daemon itself states the boundary (kortix-sandbox-agent-server/README.md:6-24): it only supervises opencode serve, reverse-proxies HTTP/SSE, health-checks, and static preview; triggers/channels/connectors/secrets are not the daemon’s job.

The SDK is the host’s only data layer (architectural iron law)

AGENTS.md:91-137 writes this in stone:

  • Logic goes into the SDK; hosts must not raw-fetch or use @opencode-ai/sdk;
  • One host, one client: createKortix / configureKortix;
  • One hook for an entire session: useSession(projectId, sessionId);
  • apps/web’s stores/server-store, hooks/opencode/use-* are shims (e.g. apps/web/src/stores/server-store.ts:2: export * from '@kortix/sdk/server-store'; hooks/opencode/use-session-sync.ts:2: export * from '@kortix/sdk/react').

Web’s only wiring port: apps/web/src/lib/kortix-config.ts:3-30 uses configureKortix({ backendUrl, getToken, … }) to inject the Supabase JWT and toast/notification sinks.

Four often-ignored packages (summary)

@kortix/manifest-schema (packages/manifest-schema/src/index.ts:1-15): the canonical schema for kortix.yaml plus a pure-function validator. Consumers: CLI ship pre-check, backend CR-merge gate, kortix validate. No I/O, no DB.

@kortix/starter (packages/starter/src/index.ts:1-48): folder templates + getStarterFiles(); the default user template is general-knowledge-worker; internally it also keeps minimal for clone/seed. API create-repo and kortix init share it.

@kortix/registry (packages/registry/src/index.ts:1-12): shadcn-compatible registry discovery/build; install has already been changed to agent import (read the source, merge files, land a CR) — there is no longer a deterministic install engine.

@kortix/executor-sdk (packages/executor-sdk/src/index.ts:41-50): a typed client that carries a token; can go through /executor/projects/:projectId/..., so “CLI on a laptop” and “session token inside the sandbox” call the same shape, while secrets stay on the server.

Design trade-offs (architecture layer)

  • Thick platform, thin host: logic goes into SDK/API; Web is a thin consumer (AGENTS.md:91-133). The cost is a huge monorepo and a long path for newcomers.
  • Truth is in the sandbox + git; the DB is a mirror: lists are fast, but debugging requires looking at three places at once (MANIFESTO.md:50).
  • Elastic license: you can self-host, but you cannot easily turn it into “someone else’s competing SaaS” — the commercial moat is written into the license.

🧠 One sentence: thick platform, thin host — all the logic is pressed into the SDK and API; Web is only a consumer; truth lives in the sandbox and git; the database is just an acceleration mirror.


Part 3

Chapter 3 · Startup flow: from command to UI

Metaphor: get a park pass → be assigned a workstation → power on → open the door for guests

3.1 Two “from zero to chat” paths: local / cloud

Scaffolding (build the company repo on a laptop)

kortix init creates a project that already has an OpenCode runtime and kortix.yaml (apps/cli/src/commands/init.ts; templates come from @kortix/starter, packages/starter/src/index.ts:1-14). You can symlink .kortix/opencode into Claude/Codex/Cursor native directories so the same agents/skills are shared by multiple coding agents.

Ship

kortix ship: validate the manifest → commit → fill secrets → push → connect connectors (apps/cli/src/commands/ship.ts help text; manifest validation goes through @kortix/manifest-schema, packages/manifest-schema/src/index.ts:5-11). The product promise is “the same repo, laptop and cloud behave the same” (MANIFESTO.md:38).

Local full-stack development

pnpm dev brings up Web(:3000) + API(:8008) + local Supabase + a cloudflared tunnel (AGENTS.md:178-193; root package.json:5). Secrets enter the repo as dotenvx ciphertext (AGENTS.md:195-202).

3.2 How a Session is “born” on the control plane

The core state machine lives in apps/api/src/projects/session-lifecycle/engine.ts:

  1. createSession (engine.ts:44)
  2. Can queue for backpressure via queuePolicy (always / on_backpressure / never).
  3. Supports idempotency keys; on conflict, checks connector bindings / secrets allowlist; a cross-tenant key collision is rejected outright.
  4. Actually building the session goes through executeCreateSession → createProjectSession / sandbox provision.

  5. Sandbox provision (apps/api/src/platform/services/session-sandbox.ts:1-12)

  6. Inserts a row into kortix.session_sandboxes; the primary key is the UUID the caller supplied, and it equals the project session id.
  7. Fire-and-forget: return provisioning first; provider create() runs in the background.
  8. When minting the executor token, a comment writes it in stone: session_id == sandbox_id, so the LLM gateway can attribute usage to that session (session-sandbox.ts:147-149).
  9. Route-layer invariant: session_id == sandbox_id == git branch name (apps/api/src/projects/routes/r7.ts:348).

  10. startSession (engine.ts:206)

  11. Calls openSession; optional waitMs is a server-side long-poll until ready or the deadline (so the client does not spin empty).

  12. Host side (Web is checked out)

  13. SDK: configureKortix / createKortix → useSession (packages/sdk/src/react/use-session.ts:3-19).
  14. Web session page: ProjectSessionPage mounts useSession(projectId, sessionId, { enabled, replayStartStash:false, chatEngine:false }) once (apps/web/src/app/(app)/projects/[id]/sessions/[sessionId]/page.tsx:46-100).

3.3 Snapshot builder: where the workstation image comes from (deepened)

apps/api/src/snapshots/builder.ts:1-14 positions the builder as a “thin orchestrator”:

  1. (project, slug) → ResolvedTemplate;
  2. Compute a content-addressed snapshot name;
  3. Ask the provider: if the image is already active, use it; otherwise inline-build.

The key philosophy (same-file comment): the boot path never trusts a DB row to decide “does the image exist” — it asks the provider every time; DB project_snapshot_builds is only cache + audit + UI “Fix with agent” (builder.ts:9-13).

Trigger-source enum (builder.ts:56-62): session-start / project-create / cr-merge / manual / background / startup — so a CR merge also drives a rebuild; company-config changes show up in the next sandbox.

Cross-replica dedup: waitForProviderBuild polls to settlement when the provider already has a same-name build; on timeout it stays building and fails closed, so you do not get two same-name builds (builder.ts:67-72).

The provider list is constrained by a single source of truth, KNOWN_PROVIDERS; the parity test requires daytona/platinum/e2b/local-docker to match the template provider set (apps/api/src/platform/providers/provider-parity.test.ts:11-34). local-docker can be admitted without a cloud-vendor API key (same file :70-74).

The sandbox image itself (apps/sandbox/README.md:15-28) only bakes git, opencode, and kortix-agent; triggers/channels/connectors/secrets do not enter the image — they rely on create-time env / HTTP.

3.4 In-sandbox boot order (daemon)

kortix-sandbox-agent-server README boot flow (README.md:6-24 + Boot flow section):

  1. The supervisor process brings up opencode serve; Hono reverse-proxies to KORTIX_SERVICE_PORT (default 8000);
  2. /kortix/health, /kortix/refresh;
  3. Static preview port KORTIX_STATIC_PORT (default 3211) — apps/web composes preview URLs with /proxy/3211/* and the p3211-<sandboxId> subdomain (daemon README:14-18);
  4. Materialize the repo first, then resolve OpenCode config (source comments emphasize the order).

3.5 Web: opening the door from project home to session page

  1. /projects/[id] renders ProjectHome (welcome wall + ComposerChatInput) (project-home.tsx:46-58; page.tsx:6-9).
  2. User sends → writeStartStash stuffs the first sentence into the SDK start-stash (page.tsx:17; use-session.ts header comment) → useNewProjectSession creates the session and navigates.
  3. Arrive at /sessions/[sessionId]: useSession owns POST /start, sandbox switch, SSE, ready-seed, and the canonical OpenCode id (page.tsx:46-59,87-100).
  4. Fresh sessions use InstantSessionShell with a cross-fade; resume uses SessionStartingLoader (page.tsx:362-410).
  5. ActiveSessionChat then mounts SessionLayout + SessionChat (page.tsx:649-659); chat-message truth comes from useSessionSync (shim → SDK) (session-chat.tsx:3482-3491).

Design trade-offs

  • Async provision + long-poll start: first packet can take minutes; queues and waitMs absorb it.
  • Snapshot treats the provider as truth: avoids DB drift; the cost is one extra remote query on every boot.
  • The page splits useSession lifecycle from the SessionChat message engine: chatEngine:false avoids double-mounting useSessionSync (page.tsx:91-95).

Part 4

Chapter 4 · Input capture and fan-out: the journey of one sentence

Metaphor: many storefronts take orders; the kitchen only recognizes one kind of “work ticket”

4.1 Entry matrix

Entry How it enters the park Critical path
Web ProjectHome / SessionChat → SDK project-home.tsx; session-chat.tsx; use-session.ts
Desktop Electron loads the Web URL apps/desktop-electron/package.json:12-14
Mobile Expo + SDK apps/mobile/package.json
CLI kortix sessions / sessions-chat apps/cli/src/commands/sessions*.ts
Slack webhook → binding → lifecycle index.ts:788-792; channels/slack/
Teams / Telegram / Email / Meet each has its own webhook index.ts:790-796
Triggers projectWebhooksApp + scheduler index.ts:775

Channel messages eventually have to become: a session on some project + a piece of text (or a first-time create+prompt).

4.2 Web composer path (deepened)

Project-home onSend: billing gate → writeStartStash(sessionId, { prompt }) → create session → router into the session page. The session-page comment is explicit: replayStartStash:false, because Web has its own pending-prompt handoff (page.tsx:91-92); ActiveSessionChat migrateStash(routeId → chatSessionId) after the canonical OpenCode id is resolved (page.tsx:521-537).

InstantSessionShell also reads/writes start-stash (instant-session-shell.tsx imports readStartStash, writeStartStash), which keeps the “sandbox is still waking, user can already type” instant feel.

4.3 Continue-chat delivery: continueSession

When the session already exists, a channel or scheduler calls continueSession (engine.ts:252):

  1. Read projectSessions; abandon immediately if failed; if metadata.deletedAt exists, treat it as deleted (even if user status is stopped) — so a Slack reply cannot “resurrect” a deleted session.
  2. stopped/completed flip back to running.
  3. Poll openSession until ready, total budget READY_DEADLINE_MS = 300_000 (5 minutes) (engine.ts:39,320).
  4. deliverWithRetry: keep delivering inside a 45s window, healing “just-woke sandbox 404/5xx” (deliver.ts:12-13,28).

What actually stuffs the utterance into the brain is postPrompt (engine.ts:668-685):

forwardToSandbox(externalId, port=8000, …)
  POST /session/{opencodeSessionId}/prompt_async?directory=/workspace
  body: { parts: [{ type: 'text', text }] }

The control plane does not parse tool_calls; it only asynchronously dumps user text into OpenCode.

4.4 SDK / cache-key fan-out

The useSession comment calls itself “the one hook a host needs to open a session and stream chat” (use-session.ts:3-19). The cache key deliberately includes activeServerKey() (the current sandbox id), because each project session is an independent sandbox (session_id == sandbox_id); otherwise switching sessions would mix data (packages/sdk/src/react/use-opencode-sessions/shared.ts:17-30).

4.5 Slack thread stickiness (short)

bindChatThread writes (platform, workspaceId, threadId) → sessionId into chat_threads — create and webhook continue-chat share the same registry, otherwise follow-ups cannot find a home.

Design trade-offs

  • Delivery and ack are decoupled: Slack acks the webhook first, then delivers in the background — you must have heal/retry (deliver.ts:7-11).
  • Delete is a soft-delete semantic: it relies on metadata.deletedAt; you cannot look only at the status enum.
  • Web instant shell vs SDK default replay: the host can turn off replayStartStash and use its own UX to hand off the first sentence.

Part 5

Chapter 5 · Context assembly: packing for the model

Metaphor: employee handbook (repo) + this work ticket (session) + badge permissions (grant)

Kortix’s context is not a giant messages[] array assembled in Python the way nanobot does it; it is:

  1. Static intelligence in the repo: agents / skills / opencode.jsonc
  2. Governance layer compiled on the server: connectors / secrets allowlist / enable flags
  3. Env and OPENCODE_CONFIG_CONTENT injected into the sandbox
  4. The session transcript OpenCode itself maintains (inside the sandbox)
  5. Manifest contract: @kortix/manifest-schema guarantees kortix.yaml has a legal shape

5.1 One concern, one place: behavior vs governance

The long comment in compile-agent-config.ts writes the architectural turn clearly:

  • Behavior (mode/model/temperature/steps/permission/prompt…) lives in .kortix/opencode/agents/<name>.md — as-is, that file is already a legal OpenCode agent file.
  • Governance (connectors/secrets/skills/kortix_cli/workspace/enabled) lives in the agents: map of kortix.yaml.
  • The compiler is a pure function: copy the md parse result into the OpenCode agent map; enabled: false forces disable; skills fold into permission.skill.

5.2 How manifest validation hard-stops bad config

The header comment of @kortix/manifest-schema lists three gates (packages/manifest-schema/src/index.ts:5-11):

  1. kortix ship pre-check (a bad manifest does not get pushed);
  2. Backend CR-merge gate (raw git / web edits that bypass the CLI are still blocked);
  3. kortix validate explicit report.

Errors are structured (path + severity + message + optional row/column); callers can color diagnostics. This contrasts with OpenManus’s “bad config only explodes at runtime” — Kortix lifts config correctness into a product latch.

5.3 How the daemon stuffs the “packed result” into OpenCode

buildOpencodeConfigContent merges several forces (Executor MCP, Kortix LLM gateway provider, Slack permission overlays, KORTIX_COMPILED_AGENT_CONFIG). If all are empty, it uses the in-repo OpenCode config entirely. The daemon only layers; it does not invent behavior itself.

5.4 Runtime env dictionary (birth certificate)

buildSessionRuntimeEnv writes the contract keys between the control plane and the daemon (see the old deepened appendix; now woven in):

Env Meaning
KORTIX_REPO_URL / KORTIX_BASE_REF which company to clone, which baseline to grow from
KORTIX_BRANCH_NAME equals sessionId — session branch name and session identity are one
KORTIX_PROJECT_ID / KORTIX_SESSION_ID coordinates when calling back the API
KORTIX_SERVICE_PORT=8000 daemon’s external port, also the convention for /v1/p/:id/8000
KORTIX_COMPILED_AGENT_CONFIG v2 compiled-product JSON string
KORTIX_INITIAL_PROMPT optional first sentence

If you understand this table, you understand where “context assembly” lands on the platform side: it is not assembling messages[]; it is writing env + writing a git branch + writing compiled config.

Design trade-offs

  • “OpenCode-native files are the behavior source” lowers dual-config drift; the cost is that the control plane must learn to read md in git.
  • Governance is not delegated into md: so an Agent cannot rewrite its own permission boundary and skip YAML — it only takes effect after a CR merges.
  • manifest-schema reused in three places: one schema serves CLI/API/future tools, avoiding “everyone writes their own YAML dialect.”

🧠 One sentence: Kortix’s “context assembly” is not assembling a message array in memory; it is writing a git branch + writing environment variables + writing compiled config — truth lands in files, not in a process.


Part 6

Chapter 6 · The agent main loop: the project’s heart

Cold water first: this repo has no while True: think(); act()

OpenManus’s heart is ToolCallAgent.think/act; nanobot’s heart is AgentRunner. Kortix’s “model heart” is inside the OpenCode process; this repo’s heart is Session Lifecycle + Daemon Supervisor + Proxy + (on the host) useSession. Putting that in the title is to stop readers from spinning in apps/api looking for ReAct.

6.0 First look at a real task: how one Slack message becomes a CR waiting for review

The abstract “three loops” sound winding. First watch a concrete task land. A colleague @’s the bot in Slack: “Make the landing-page title a bit more forceful.” Follow that sentence and you will see which baton each loop takes:

Stage Who is doing it What they did Source anchor
① Take the order Platform loop Slack webhook comes in; find (or create) this conversation by (platform, workspace, thread) → session index.ts:788-792; bindChatThread
② Wake the workstation Platform loop continueSession: if the session was hibernating, wake the sandbox; wait up to 5 minutes until “ready” engine.ts:252; READY_DEADLINE_MS
③ Deliver the utterance Platform loop deliverWithRetry keeps delivering inside a 45-second window, healing “the just-woke sandbox has not recovered yet” deliver.ts:28; postPrompt (engine.ts:668-685)
④ Actually think OpenCode loop OpenCode in the sandbox reads the repo, thinks one step, edits the landing-page file, thinks again — this is where ReAct happens inside the OpenCode process (not implemented in this repo)
⑤ Keep a record OpenCode loop After editing, it does not go straight to trunk; it git commit + kortix cr open to open a change request cr.ts:38-40
⑥ Report back Host loop If you have Web open, useSession paints every step onto the screen in real time via SSE use-session.ts:3-19
⑦ Govern Platform loop The CR appears in Review Center; only after a human clicks merge does the company actually change; merge also triggers a sandbox-image rebuild r9.ts:19-21; builder source cr-merge

Once you understand this table, you have Kortix’s biggest difference from OpenManus: the endpoint of a task is not “the model finished speaking”; it is “produce a CR a human can review.” The platform loop is responsible for reliably delivering the utterance and waking the workstation; real thinking and doing happen in OpenCode; and every change must pass the change-request human gate before it lands in “the company,” this Git repository.

flowchart LR
    SLACK["💬 Slack: tweak the landing-page title"] --> PLAT
    subgraph PLAT["① Platform loop (this repo)"]
        FIND["Find the session"] --> WAKE["Wake the sandbox"] --> SEND["Reliably deliver the utterance"]
    end
    SEND --> OC
    subgraph OC["② OpenCode loop (inside the sandbox)"]
        THINK["Read repo → think → edit files → think again"] --> COMMIT["git commit + kortix cr open"]
    end
    COMMIT --> CR["📋 change request (open)"]
    CR --> REVIEW["🧑‍⚖️ Human reviews in Review Center"]
    REVIEW -->|"merge"| MAIN["Company trunk changed
+ trigger image rebuild"] OC -.->|"SSE live report"| HOST["🖥️ Host loop useSession"] style PLAT fill:#eaf1ff style OC fill:#e7f8f1 style CR fill:#fff3df style MAIN fill:#fff3df

🧠 One sentence: the platform loop is responsible for “reliably delivering the utterance and waking the workstation”; the OpenCode loop is responsible for “think and do”; the host loop is responsible for “live display” — three batons in a relay; the finish line is a CR waiting for human review, not a sentence that says “I’m done.”

6.1 Separate the two loops (this chapter’s core)

flowchart TB
    subgraph PLATFORM["Platform loop (implemented in this repo)"]
      C["createSession"] --> P["provisionSessionSandbox"]
      P --> S["startSession / openSession"]
      S --> D["continueSession / deliverWithRetry"]
      D --> PP["postPrompt → prompt_async"]
      S --> STOP["stop / restart / queue drain"]
    end
    subgraph OPENCODE["OpenCode loop (sandbox binary)"]
      T["Read transcript + tools"] --> M["Call LLM (via gateway)"]
      M --> TOOL["Local tools / Executor / CLI"]
      TOOL --> T
    end
    subgraph HOST["Host loop (SDK + Web)"]
      US["useSession: /start + SSE + sync"] --> UI["SessionChat renders turns"]
    end
    PP --> OPENCODE
    US --> S
    OPENCODE -->|SSE via /v1/p| US
Question Platform loop answers OpenCode loop answers
Is this workstation ready? stage==='ready' (server-truth) —
Did this sentence get in? deliverWithRetry / postPrompt receives prompt_async
Which tool next? not responsible model + tool schema
Where does the message list come from? DB mirror can sync in-sandbox transcript is truth
How does the host subscribe? — SSE; useSession / useSessionSync

6.2 Platform heart: the session-lifecycle state machine

stateDiagram-v2
    [*] --> Creating: createSession
    Creating --> Queued: backpressure
    Queued --> Creating: drain queue
    Creating --> Provisioning: insert session_sandboxes
    Provisioning --> Ready: openSession stage=ready
    Provisioning --> Failed: provider/boot error
    Ready --> Running: prompt_async / user chat
    Running --> Stopped: stop/hibernate
    Stopped --> Running: continueSession wake
    Running --> Failed: terminal error
    Ready --> Deleted: deleteSession metadata.deletedAt

Key code anchors:

  • Create and idempotency: engine.ts:44
  • Start and long-poll: engine.ts:206
  • Continue-chat and 5-minute ready wait: engine.ts:252,39,320
  • Delivery heal: deliver.ts:28
  • postPrompt: engine.ts:668-685

openSession (projects/routes/shared.ts) is responsible for resolving DB row + provider state into stage, and can resume when stopped / provision when there is no box — what continue waits for is not just “read status”; it may be waking a hibernated disk.

6.3 Sandbox heart: supervise OpenCode, do not replace it

Daemon duty list (README:6-12):

  1. spawn / crash-restart / signal-drain opencode serve
  2. Reverse-proxy its HTTP + SSE to port 8000
  3. /kortix/health, /kortix/refresh
  4. Static preview port 3211

After ready, it lowers liveness frequency (so idle sandboxes do not eat a full CPU) — typical platform-taste optimization.

6.4 Host heart: the useSession mount contract on Web

SDK header comment (use-session.ts:3-19):

Everything sandbox-shaped is internal… The host imports createKortix + useSession and NOTHING else runtime-related.

The Web session page executes this faithfully (page.tsx:46-100):

  • Mount useSession(projectId, sessionId) once;
  • Billing gate blocks accounts with no plan, so you do not poll forever a sandbox that will never provision;
  • chatEngine:false: the page only eats boot/lifecycle; SessionChat itself mounts useSessionSync;
  • Mid-session reconnects use useSandboxConnection; initial ready trusts server stage (page.tsx:55-58,419-424).

This is why Chapters 4/6 talk about both the “platform loop” and the “host loop”: without useSession, Web would still hand-roll a 7-step mount — the page-header comment says they already deleted that set (page.tsx:49-51).

6.5 End-to-end timing of one user turn

sequenceDiagram
    participant U as User/Web/Slack/CLI
    participant SDK as @kortix/sdk
    participant API as apps/api
    participant P as /v1/p proxy
    participant D as kortix-agent
    participant O as opencode
    participant X as /v1/executor
    participant G as LLM gateway

    U->>SDK: useSession / send
    SDK->>API: /start + stream
    API->>API: continueSession / startSession
    API->>P: forwardToSandbox prompt_async
    P->>D: POST /session/:id/prompt_async
    D->>O: reverse proxy
    O->>G: chat.completions
    O->>X: connector call (scoped token)
    X-->>O: tool result
    O-->>D: SSE events
    D-->>P: SSE
    P-->>SDK: stream
    SDK-->>U: SessionChat turns
    Note over O: afterwards the agent can git commit + kortix cr open

6.6 Faults and self-heal

  • Delivery layer: 45s deliverWithRetry (deliver.ts).
  • Web layer: a hibernated but resumable sandbox will automatically invalidate /start at most 3 times (page.tsx:106-139).
  • Daemon layer: on recoverable errors it can auto-resume a turn (source turn-auto-resume.ts; sub-agent failure is left to the parent model).
  • Control plane: dead-letter marks the session failed.

Design trade-offs

  • Reuse OpenCode = stand on a giant’s shoulders: you inherit the tool ecosystem immediately; the cost is debugging across three processes: API + daemon + OpenCode.
  • The platform loop is extremely heavy: idempotency, backpressure, cross-tenant isolation, delete guards — the SaaS multi-tenant tax OpenManus’s single-machine script does not have to pay.
  • The host loop collapses into one hook: when you swap in mobile/whitelabel you reuse the same ready semantics.

🔍 Source pointers (go check the source): session-lifecycle/engine.ts:44 (createSession)|:206 (startSession long-poll)|:252 (continueSession)|:668-685 (postPrompt → prompt_async)|deliver.ts:28 (delivery heal)|kortix-sandbox-agent-server/README.md:6-24 (daemon supervises OpenCode)|packages/sdk/.../use-session.ts:3-19 (host’s only hook)

Contrast with OpenManus (short note)

OpenManus: BaseAgent.run → think/act in the same process. Kortix: the platform only guarantees “workstation ready + the utterance got in”; thinking and tools are in another process. When choosing, do not pair engine.ts against ToolCallAgent.py.


Part 7

Chapter 7 · Tool system and permissions: the model’s “hands” and “tight hoop”

7.1 Three pairs of hands

  1. OpenCode local tools: files, shell, its plugins — they run inside the sandbox OS, naturally isolated by session branch. Web uses features/session/tool/tools/* to render these tool results (e.g. bash-tool.tsx, read-tool.tsx, task-tool.tsx); it does not execute them in the browser.
  2. Kortix Executor (connectors): OpenAPI/HTTP/MCP etc., sent from the API process; credentials are assembled on the server (execute.ts:1-5). The sandbox only sees a scoped token. Clients can use @kortix/executor-sdk (packages/executor-sdk/src/index.ts:41-50).
  3. In-sandbox kortix CLI: cr / secrets / sessions… callbacks with KORTIX_CLI_TOKEN (cr.ts:38-40).

7.2 Executor: secrets never cross the border

This is the most important move in Kortix’s entire security model; pin it with one diagram first: the Agent in the sandbox never gets real credentials. When it wants to call a third party (send Slack, write Notion), it can only hand the request to the server-side Executor; the real API key is attached only on this side of the API process; from start to finish the sandbox holds only a restricted temporary token.

flowchart LR
    subgraph SBX["Sandbox (session workstation) · scoped token only"]
        AGENT["OpenCode Agent
“help me call connector X”"] end subgraph API["apps/api process (trusted) · keeps real credentials"] POLICY{"policy engine
is this action allowed?"} SECRET["Take out resolved secret
attach auth headers"] FETCH["fetch third party"] end THIRD["🌐 Third-party API"] AGENT -->|"binding + args
no real key"| POLICY POLICY -->|"always_run"| SECRET POLICY -->|"require_approval"| WAIT["🧑 Wait for a human to nod in Review Center"] POLICY -->|"block"| DENY["Deny"] WAIT -->|"approved"| SECRET SECRET --> FETCH --> THIRD THIRD -->|"result"| AGENT style SBX fill:#fdeded style API fill:#eaf1ff

execute.ts opens with the contract: given binding + resolved secret + args, here attach auth headers and fetch; sandbox never sees credentials (execute.ts:1-5). API mount: /v1/executor (index.ts:772).

The policy engine (executor/policy.ts) evaluates always_run | require_approval | block by glob; it can link with Review Center executor-approval rows (see Chapter 10). Its default gear is clever — under default_mode = risk, reads go through directly; writes/dangerous ops need a human nod (policy.ts:126-129,161-176), much safer than “allow everything,” and not so annoying that every read needs confirmation.

🧠 One sentence: the Agent only hands over the request, never touches the key; real credentials are locked in the API process. Dangerous actions default to needing a human nod — that is “secrets never cross the sandbox boundary.”

7.3 Agent permissions and Grant

  • The permission tree in Markdown frontmatter is issued with OpenCode behavior config.
  • Account-level agent grant: resolveAgentGrant when mintExecutorToken (near session-sandbox.ts:147-154); fail-safe on failure.
  • IAM: session-gate.ts is human login-session policy — same name, different object from “project session sandbox.”

7.4 Sandbox and network boundary

  • Providers: daytona / platinum / e2b / local-docker (provider-parity.test.ts:11-34).
  • Preview proxy: /v1/p/:sandboxId/:port/* must authenticate + rate-limit.
  • Snapshot upload URLs have SSRF-protection tests — platform security awareness is stronger than OpenManus’s default-open browser tools.

7.5 Approval and the human latch

  • Review Center: native review items + adapted CRs + executor approvals (review-center-connected.tsx:3-14).
  • Change request: only merging into main changes company truth (full path in Chapter 10).
  • Web sessions also have permission / question self-heal (SDK usePermissionSelfHeal / useQuestionSelfHeal, session-chat.tsx:151-154).

Design trade-offs

  • Connectors execute centrally: friendly to security and audit; the cost is one extra hop.
  • UI only renders tools: Web’s tool components are a presentation layer, so execution logic does not leak into the browser.
  • Dual-token model (user JWT vs sandbox CLI/executor token): higher mental load, but it separates “human” from “workstation robot.”

Part 8

Chapter 8 · Context compaction and memory

Honest inventory

Capability Status in this repo
Conversation transcript storage sandbox OpenCode + DB sync mirror (MANIFESTO.md:50)
Company long-term memory files into the repo (MANIFESTO.md:67: “Memory — files for now…”)
LLM auto-compaction / Dream Dream / company-brain compound implementation not found (vision is in the MANIFESTO)
OpenCode compress tool SDK tool registry has a display label; execution is inside OpenCode
Web tool rendering dcp-compress-tool.tsx / dcp-distill-tool.tsx etc. are UI presentation
prompt cache platform-level unified implementation not found
Context-window field model-catalog metadata includes context window — it is not a compressor

How memory becomes a “compound”

MANIFESTO strategy: skills and memory live in the company repo as Markdown/files, evolving with CRs (MANIFESTO.md:67). This is different from Raven/EverOS vector memory — Kortix first bets on git-diffable text memory.

The SDK provides formatTranscript to export a session as Markdown (packages/sdk/src/index.ts transcript export), convenient for humans to read and archive, but it is not an automatic compressor.

Design trade-offs

  • Memory = versioned files: reviewable, rollbackable, greppable; long sessions still depend on OpenCode’s own context management.
  • For nanobot users who “want a long-resident private-chat butler”: Kortix leans toward “project/company working memory,” not an IM personal diary.

Part 9

Chapter 9 · Sub-agents and multi-agent: the cloning trick

9.1 Multiple Agents in the same project (role clones)

  • Multiple .md agents + agents: governance items in kortix.yaml.
  • mode: primary | subagent | all appears in compiled config — semantics are interpreted by OpenCode.
  • Web customize → Agents view can edit (features/workspace/customize/sections/view/agents-view.tsx).
  • SDK useVisibleAgents lets the session side choose visible Agents.

9.2 Massive parallelism of the same config (workstation clones)

This is Kortix’s key difference from OpenManus PlanningFlow: parallelism comes from isolated sandboxes, not from scheduling multiple roles in one process. MANIFESTO.md:48: fifty sessions do not pollute each other; conflicts are handed to git merge. Each session is independent session_id == sandbox_id (SDK shared.ts:17; r7.ts:348).

9.3 Marketplace and inheritance

  • apps/api/src/marketplace/: catalog-style distribution.
  • @kortix/registry: discovery/build; install goes through agent import + CR (packages/registry/src/index.ts:8-11).
  • CLI kortix agents / marketplace.

9.4 Where sub-agent failure belongs

Daemon auto-resume comments lean: Task/subagent failure should be handled by the parent model. Web has renderers such as task-tool.tsx, agent-spawn-tool.tsx, proving nesting happens at the OpenCode tool layer.

Design trade-offs

  • Parallel = many sandboxes: high bill and cold-start cost; the correctness model is simple (git).
  • OpenManus-style PlanningFlow: isomorphic implementation not found in this repo; long-task planning depends more on Agent prompt / skills / human CR cadence.

Contrast with OpenManus (short note)

OpenManus uses PlanningFlow to dispatch [AGENT] step by step in one process; Kortix trades isolation and auditability with “many sandboxes + many agent md + CR.”


Part 10

Chapter 10 · Ecosystem: commands, extensions, and the full Change Request path

10.1 CLI surface (the company remote)

Visible under apps/cli/src/commands/: init, ship, sessions, sessions-chat, cr, agents, skills, connectors, secrets, triggers, marketplace, channels, executor, sandbox(es), login/logout, self-host, doctor… — covering the closed loop from scaffolding to approval.

10.2 Change Request full path (this chapter’s hard goods)

Data model

packages/db/src/schema/kortix.ts:2719-2755:

  • Status enum: open | merged | closed (:2726-2730)
  • Key columns: number, base_ref/head_ref, head_commit_sha, origin_session_id, merge_commit_sha…
  • Comment: a CR is metadata; the real fetch/diff/merge goes through apps/api/src/projects/git.ts; the backend can be GitHub/GitLab/pure git (:2720-2724).

Serialization and “request changes” notes land in metadata.requested_changes (apps/api/src/projects/change-requests.ts:1-14,72-95).

API routes

Method Path File
GET /:projectId/change-requests routes/r8.ts:225-227
POST /:projectId/change-requests r8.ts:272-274
GET/PATCH /:projectId/change-requests/:crId r8.ts:547-586
POST …/request-changes r8.ts:641-643
GET …/diff, …/merge-preview r8.ts:731-796
POST …/merge r9.ts:19-21 (and the tail of r8)
POST …/close, …/reopen r9.ts:180-229

After a successful merge it can trigger connector reconcile and a snapshot rebuild sourced as cr-merge (builder trigger-source enum; r9.ts merge handling).

CLI

kortix cr (apps/cli/src/commands/cr.ts:13-40): ls/show/diff/open/merge/close/reopen. Inside the sandbox it automatically reads KORTIX_CLI_TOKEN + KORTIX_PROJECT_ID (:38-40) — the Agent does not need to log in.

Web UI

  1. Project-files side: change-requests-panel.tsx, change-request-detail-dialog.tsx, open-change-request-dialog.tsx; hooks features/project-files/hooks/use-change-requests.ts.
  2. Sidebar entry: project-change-requests-nav.tsx.
  3. Review Center (real data): ReviewCenterConnected (review-center-connected.tsx:3-14,88-148)
  4. CR approve → useMergeChangeRequest
  5. reject → useCloseChangeRequest
  6. request changes → persist notes + deliver back to the originating agent (backend can wake the sandbox)
  7. cr: adapted ids do not go through the generic /act (it would 409); they must go through the source flow (:107-109)
  8. /review route: mock prototype only (review/page.tsx:8-18); do not treat it as the production API. The production entry is the Review view in project customize (review-view.tsx:4-32).
  9. SDK: packages/sdk/src/core/rest/projects-client/change-requests.ts; packages/sdk/src/react/use-change-requests.ts.

Agent-side docs/skills

The starter template includes change-requests reference docs (packages/starter/templates/base/.kortix/opencode/skills/kortix-system/references/kortix/change-requests.md); Web docs: apps/web/content/docs/work/change-requests.mdx.

Lifecycle in one sentence

commit inside the sandbox → kortix cr open
  → DB change_requests(status=open)
  → Web Review / CLI show/diff
  → merge → git merge + status=merged + (optional) snapshot rebuild
  → or close / request-changes → deliver again for the agent to revise

10.3 Connectors and MCP / registry / starter

  • Pipedream and other catalogs live in apps/api/src/executor/.
  • @kortix/executor-sdk gives external TS code isomorphic calls.
  • @kortix/registry + API marketplace: discovery and agent import.
  • @kortix/starter: unified scaffolding, so CLI/API do not each write a template set.

10.4 Channel ecosystem

Slack/Teams/Telegram/Email/Meet webhooks enter the API (index.ts:788-796). Slack sessions rewrite OpenCode permission (to avoid getting stuck on question).

10.5 How to extend (for second-party developers)

  1. Add an agent .md / skill in the repo, open a CR.
  2. Hang connector / trigger / sandbox specs on kortix.yaml (pass manifest-schema first).
  3. Go through marketplace agent import.
  4. Build your own connector and expose it via the executor protocol.
  5. Deep change: touch @kortix/sdk (TDD and export contract, AGENTS.md:99-108) or the daemon.

10.6 Migration and historical baggage

projects/suna-migration/: migrate from the old Suna/AgentPress world to OpenCode DB mapping — proof the product went through one “runtime heart transplant.”

🧠 One sentence: Change Request is Kortix’s product primitive, not an accessory. Whatever the Agent changed does not count until a CR is merged into trunk by a human — that is the landing form of “AI does the labor, humans govern.”


Part 11

Chapter 11 · Feature set: auth, models, and “thinking”

11.1 Auth

  • Humans: Supabase JWT; Web AuthProvider (apps/web/src/features/providers/auth-provider.tsx:29+) + configureKortix({ getToken }) (kortix-config.ts:30-32).
  • Programs: API key / PAT / account token.
  • Sandbox: KORTIX_TOKEN / CLI token / executor token, layered.
  • Preview: POST /v1/p/auth plants a cookie.
  • Enterprise: SCIM (/scim/v2), SSO-related oauth routes.
  • Session gate: lifetime / idle / revoked (IAM session-gate) — human sessions.

11.2 Model routing and gateway

  • API mountLlmGateway (index.ts:740-741) + /v1/router.
  • Project-level routing policy; Web customize has a Gateway view (gateway-routing.tsx etc.) + useGatewayRoutingPolicy.
  • Daemon: when a gateway exists, OpenCode goes through a synthetic kortix provider.
  • Billing: billing/ + compute metering; the Web session page has can_run / upgrade-dialog gates (page.tsx:66-85).

11.3 “Thinking” / Reasoning

  • Gateway wire can carry reasoning_effort; project policy can clamp it.
  • Web has reasoning-effort-selector.tsx (wired to useGatewayRoutingPolicy).
  • When sharing a session externally, the API can omit reasoning and mark reasoning_omitted.
  • Not found: a first-class cross-model thinking-blocks peel-off service; rendering is mainly in the SessionChat turn UI.

11.4 Other features, quick lookup

Feature Status Evidence
Multi-channel yes index.ts webhooks
Change Request yes DB + r8/r9 + CLI + Web Review
Review Center yes (real data is in-project; /review is mock) review-center-connected.tsx; review/page.tsx
Preview/static site yes daemon 3211; /v1/p
Tunnel yes /v1/tunnel
Self-host has an entry CLI self-host; self-host/
Desktop / Mobile yes apps/desktop-electron; apps/mobile
Web Command Center checked out apps/web full paths as above

Part 12

Chapter 12 · Summary: design philosophy and trade-offs

flowchart TB
    GOAL["Goal: a self-hostable company-grade AI Command Center
company = Git repo"] GOAL --> P1["Pillar 1 session = isolated sandbox
session_id == sandbox_id == branch
→ Chapters 3/6"] GOAL --> P2["Pillar 2 outsource the brain to OpenCode
platform only does lifecycle/proxy/config
host only mounts useSession
→ Chapters 5/6"] GOAL --> P3["Pillar 3 change must go through a CR
API+CLI+Review Center
→ Chapters 7/10"] P1 & P2 & P3 --> BASE["Foundation: SDK SSoT + manifest-schema + Executor keeps secrets
+ LLM Gateway + multi-provider snapshot
→ Chapters 2/3/7/11"]

Five distinctive ideas

① The company is literally a repository. agents/skills/memory/sandbox specs all go into the repo; greppable, diffable (MANIFESTO.md:27-36).

② session_id == sandbox_id == git branch. Route comment (r7.ts:348); executor-token minting comment (session-sandbox.ts:147-149); SDK cache-key comment (shared.ts:17).

③ Platform loop ≠ model loop ≠ host loop. The API intensively farms create/start/continue/deliver; ReAct is left to OpenCode; Web uses useSession to eat server-truth ready (Chapter 6).

④ Secrets never cross the sandbox boundary. Executor attaches credentials on the server; executor-sdk likewise.

⑤ Change Request is a product primitive. Schema (kortix.ts:2719+) + API r8/r9 + CLI cr + Review Center adapter — “AI labor + human governance.”

Main costs

  • Complexity tax: monorepo + multi-provider + queues + idempotency + SDK contract; the learning curve is far above OpenManus.
  • Cold-start tax: snapshot/sandbox at minute scale; compensated with warm, long-poll, deliver heal, Web instant shell.
  • License tax: Elastic 2.0 restricts hosting a competing product (LICENSE:18-20).
  • The brain is invisible: deep Agent behavior requires reading OpenCode upstream; this-repo analysis cannot replace that.

Sideways comparison at a glance (for the series docs)

Dimension Suna / Kortix
Team / license kortix-ai; Elastic License 2.0
Stack TS/Bun monorepo + Hono + Supabase + Next + OpenCode-in-sandbox
Architecture control-plane session-lifecycle + sandbox daemon + OpenCode; SDK SSoT
Models LLM Gateway routing; OpenCode kortix/ provider
Tool scale OpenCode tools + server-side Executor/MCP + built-in CLI
Extension yaml + md agents/skills + registry/marketplace + connectors
Main loop platform: session-lifecycle/engine.ts; model: OpenCode; host: useSession
Permissions IAM + agent grant + executor keeps secrets + CR governance
Compaction / memory repo-file memory; platform-level LLM compaction not seen
Frontend full checkout: Command Center + Session + Review

One-sentence close: Claude Code answers “how to write the code well in this repo”; OpenManus answers “how to use a readable ReAct chain for research deliverables”; nanobot answers “how to let a personal Agent live long-term in a chat app”; and Kortix/Suna answers — how to turn a company into a cloneable Git repository, so that hundreds or thousands of isolated-sandbox OpenCode laborers, under change-request discipline, improve the company itself day and night.

Suggested reading order (full checkout)

  1. MANIFESTO.md → worldview
  2. AGENTS.md SDK iron laws + apps/web/src/lib/kortix-config.ts
  3. packages/sdk/src/react/use-session.ts + Web sessions/[sessionId]/page.tsx
  4. session-lifecycle/engine.ts create/start/continue/postPrompt
  5. snapshots/builder.ts + session-sandbox.ts header comments
  6. kortix-sandbox-agent-server/README.md
  7. change-requests.ts + routes/r8.ts/r9.ts + review-center-connected.tsx
  8. packages/manifest-schema / starter / registry / executor-sdk each package.json + src/index.ts

Naming mines

Name you see What it actually means
Suna historical product/repo name; migration code in suna-migration/
Kortix current product name, CLI, npm scope @kortix/*
session (IAM) human login session
session (project) Agent workstation; often the same id as sandbox
sandbox_id on most paths == project session id
opencode_session_id OpenCode internal conversation id, not equal to project session id
/review vs Review Center the former is mock; the latter is in-project Connected real data
VERSION 0.10.14 vs badge 0.9.98 treat the VERSION file as canonical

Part 13

Deepen-weave A: Web Command Center UX close read (reinforces Chapters 2/3/4)

A.1 Routes and shell

Under the App Router, the (app) group is the main logged-in shell. Project page /projects/[id] does not chat directly; it is ProjectHome: welcome area + composer + shortcut entries (Agents / Secrets / Sandbox / Triggers / Marketplace icon buttons, see the second half of project-home.tsx). On send, the parent ProjectIndexPage (page.tsx:24+) is responsible for:

  1. useProjectCanRun / useAccountState billing gate;
  2. writeStartStash writes the first sentence;
  3. useNewProjectSession creates the session and navigates to /sessions/[sessionId].

The session-page comments cut responsibility cleanly (sessions/[sessionId]/page.tsx:46-59): the SDK owns runtime lifecycle; the page keeps the billing gate, instant-shell/loader cross-fade, fresh-session hand-off, restart/error cards.

A.2 useSession parameter semantics (the host must understand)

useSession(projectId, sessionId, {
  enabled: !!user && !billingGatePending && !noPlan,
  replayStartStash: false,
  chatEngine: false,
})

Evidence: page.tsx:96-100.

Parameter Meaning
enabled do not fire /start when there is no user / billing pending / no plan, to avoid spinning empty
replayStartStash Web turns off SDK default replay; uses migrateStash + InstantShell instead
chatEngine when false, do not mount useSessionSync at the page layer; hand it to SessionChat

Inside the SDK it still: long-polls /start, writes readiness into the connection store, opens SSE, resolves the canonical OpenCode id (use-session.ts:5-16). Fresh-session 404 has a grace retry: FRESH_START_404_RETRIES = 12 × 800ms (use-session.ts:66-70,79-88).

A.3 Shim inventory (proof of thin consumer)

Web path Actual export
apps/web/src/stores/server-store.ts:2 @kortix/sdk/server-store
apps/web/src/hooks/opencode/use-session-sync.ts:2 @kortix/sdk/react
apps/web/src/hooks/opencode/use-canonical-opencode-session.ts:2 @kortix/sdk/react
apps/web/src/hooks/opencode/use-opencode-events.ts SDK react re-export
apps/web/src/lib/api-client.ts:1 @kortix/sdk/api-client
apps/web/src/lib/opencode-sdk.ts:1 @kortix/sdk/opencode-client

AGENTS.md:128-133 is explicit: on conflict keep the shim, port new logic into the SDK.

A.4 Session feature directory map (features/session/)

After full checkout this directory is huge (200+ files scale). Responsibilities layered:

  • Shell: session-layout.tsx, instant-session-shell.tsx, session-starting-loader.tsx, sandbox-loading-boundary.tsx
  • Chat core: session-chat.tsx (thousands of lines: turns, optimistic send, permission/question)
  • Composer: composer-chat-input.tsx, session-chat-input
  • Tool rendering: tool/tools/register.ts registers bash/read/write/grep/task/connector/executor/dcp-compress…
  • Resume semantics: session-resume.ts, session-terminal-state.ts

Tool renderers only draw UI; they do not execute tools — execution is in sandbox OpenCode (Chapter 7).

A.5 Do not mix the two Review Center entries

Entry Nature Evidence
/review clickable mock prototype, no API/auth review/page.tsx:8-18
Project Customize → Review Connected real data review-view.tsx:4-32; review-center-connected.tsx

ReviewCenterConnected also:

  • Pulls useReviewItems;
  • Maps API → inbox view model (mapApiReviewItem);
  • Executor approvals go through resolveApproval (same client as in-session approval prompts);
  • CRs go through merge/close/request-changes (:88-148);
  • Read-only when canAct=false (:46-49).

Sidebar project-change-requests-nav.tsx and the session list read useReviewCenterEnabled / useReviewSessionSummary, exposing pending-review counts on the nav.


Part 14

Deepen-weave B: Snapshot / Provider / Boot close read (reinforces Chapters 3/6)

B.1 Provision and builder call chain

session-sandbox.ts:1-12: after inserting session_sandboxes (primary key = caller UUID = session id), fire-and-forget. Background then goes ensureSandboxImage (imported from builder.ts, session-sandbox.ts:39-44) then getProvider().create().

Builder SnapshotBuildSource (builder.ts:56-62) shows images are not only built on “first session”:

  • project-create: pre-warm when creating the project;
  • cr-merge: rebuild after manifest/Dockerfile change;
  • session-start: fill in when the image is missing;
  • manual / background / startup: ops and pre-warm paths.

B.2 Provider single source of truth

provider-parity.test.ts:29-31: config.KNOWN_PROVIDERS must equal the SANDBOX_TEMPLATE_PROVIDERS set. The four listed: daytona, platinum, e2b, local-docker (:11,34). Local development can use local-docker and does not require a cloud API key (:70-74).

Selection/balancing: session-sandbox.ts imports selectProvider, readActiveRouting, providerFallbackSetting — supports routing switch and fallback, not a hard-coded vendor.

B.3 Boot-by-template-id

sessionBootByTemplateIdEnabled (session-sandbox.ts:161-169) defaults ON: boot by the already-activated pinned template id; only fall back to name-boot on GC’d-pin 404. You can emergency-off with env KORTIX_SESSION_BOOT_BY_TEMPLATE_ID=0 — a production-rollout escape hatch.

B.4 Sandbox image boundary (emphasized again)

apps/sandbox/README.md:15-28: the image only has what a running session needs; triggers/channels/connectors/secrets are not in the image. This matches the MANIFESTO “inject secrets, connectors go through a proxy” (MANIFESTO.md:62-63), and also explains why changing the daemon requires rebuilding the snapshot (the feedback loop is on the slow side).

B.5 openSession wakes hibernation

openSession is not just a DB read: when stopped + has externalId it may resumeStoppedSandbox (keep disk/workspace); when there is no usable box it can provision on the open path. So continueSession’s 5-minute wait window (READY_DEADLINE_MS, engine.ts:39) covers real-world “resume or create” time, not mere spinning.


Part 15

Deepen-weave C: Change Request front/back correspondence table (reinforces Chapter 10)

C.1 State machine

stateDiagram-v2
    [*] --> open: POST change-requests / kortix cr open
    open --> merged: POST .../merge
    open --> closed: POST .../close
    closed --> open: POST .../reopen
    open --> open: POST .../request-changes\n(metadata + optional deliver to agent)

Enum definition: kortix.ts:2726-2730. After merge, write merged_at / merged_by / merge_commit_sha (table fields, kortix.ts:2757+).

C.2 “Request changes” is not an empty comment

recordRequestedChange (change-requests.ts:85-95) appends the human note into metadata.requested_changes. API …/request-changes (r8.ts:641-643) after persist may try to send the prompt back to the originating session (delivery failure only warns; the note is not dropped — see the request-changes prompt not delivered log path in r8).

Web Connected layer: if there is no note, toast asks you to write an explanation first (review-center-connected.tsx:128-134); on success, distinguish “already sent to the agent” vs “saved only” by res.delivering (:139-144).

C.3 CLI ↔ API mapping

CLI (cr.ts) Roughly the API
cr ls GET .../change-requests
cr show GET .../change-requests/:id
cr diff GET .../diff
cr open POST .../change-requests
cr merge POST .../merge
cr close / reopen POST .../close / .../reopen

Login-free inside the sandbox: env vars KORTIX_CLI_TOKEN + KORTIX_PROJECT_ID (cr.ts:38-40). Note KORTIX_TOKEN is the sandbox service key, not the CLI token (comment in the same place).

C.4 Product difference from GitHub PRs

The CR layer is not bound to a single hoster (change-requests.ts:4-8; kortix.ts:2720-2724). Permissions, notifications, Review Center can live on Kortix IAM. The cost is that “click Merge in the GitHub UI” will not automatically sync Kortix CR status — unless you go through the Kortix merge API (or a later integration; this analysis does not invent bidirectional sync that was not found).

C.5 E2E / documentation anchors

  • API script: apps/api/scripts/e2e-change-requests.sh
  • CLI script: apps/cli/scripts/e2e-cr.sh
  • Flow tests: tests/src/flows/change-requests.flow.ts
  • User docs: apps/web/content/docs/work/change-requests.mdx
  • Design-doc citation: docs/REVIEW_CENTER_DESIGN.md (Connected header comment, review-center-connected.tsx:14)

Part 16

Deepen-weave D: four packages expanded again (reinforces Chapters 2/5/10)

D.1 @kortix/manifest-schema

  • Package description: shared by CLI ship pre-check + backend CR-merge gate + validate (package.json:4; src/index.ts:5-11).
  • Pure function: (rawToml: string | object) → ManifestValidationResult (index.ts:14-15).
  • Constants surface is wide: channel platforms, connector auth/policy, sandbox CPU/memory/disk bounds, trigger types, grantable CLI actions (index.ts:22-36 imported from ./constants).
  • v2 agent validation is split into index.v2.ts (index.ts:42-48).
  • connector-headers is an independent module: forbid certain header names, limit length/count — API parser and executor share the same rules (index.ts:64-66).

D.2 @kortix/starter

  • getStarterFiles() walks the directory + {{var}} substitution, returns { path, content }[] (src/index.ts:4-9).
  • User default template: general-knowledge-worker; minimal is internal clone seed only (index.ts:36-48).
  • When compiled into the CLI binary, embedded.generated.json is the fallback (index.ts:20-27) — editing templates in the source tree hot-updates; compiled binaries do not lose templates.

D.3 @kortix/registry

  • Provides buildRegistry / loadItem / loadRegistry / validate (src/index.ts:1-12 export list).
  • Deliberately has no deterministic install engine: marketplace install is changed to the agent reading the source, merging files, and opening a CR (index.ts:8-11). This is consistent with “the company is git + CR discipline” — install itself is also a reviewable change.

D.4 @kortix/executor-sdk

  • Elastic-2.0 publishable package (package.json:5-6).
  • ExecutorClientOptions: when projectId is present, go through the project-explicit route; user token and sandbox session token are isomorphic (src/index.ts:41-50).
  • Results can carry pending_approval + execution_id (index.ts:28-38) — the same business object as Review Center executor rows and in-session approval UI.

Part 17

Deepen-weave E: platform delivery vs host send (reinforces Chapters 4/6)

E.1 Control-plane delivery (channels / automation)

continueSession
  → wait ready (≤300s)
  → deliverWithRetry (≤45s, interval 1.5s)
  → postPrompt → POST .../prompt_async

deliver.ts:2-11 explains the motive: the old path, on first 404/5xx, told the Slack user “say it again” and dropped the message; now, given the webhook is already acked, it keeps healing.

E.2 Host send (inside a Web session)

SessionChat uses SDK primitives such as sendAndRecover / beginOptimisticSend / classifySendError (session-chat.tsx:142-154); message truth comes from useSessionSync(sessionId) (:3482-3491). Billing 402 becomes a KortixSendError of kind billing (use-session.ts:96-107); the page can pop upgrade.

Both eventually land on OpenCode’s prompt/stream, but:

  • The channel path emphasizes server-side heal;
  • The Web path emphasizes optimistic UI + SSE sync.

E.3 Proxy chain (SSE return path)

  1. forwardToSandbox / host via /v1/p/<external>/8000/...
  2. sandbox-proxy auth
  3. daemon reverse-proxies opencode serve
  4. SDK openEventStream (framework-agnostic, reusable in a worker) manages timeout, heartbeat, reconnect backoff

AGENTS.md writes that the proxy URL looks like http://localhost:8008/v1/p/<external_id>/8000/..., SSE at …/event (AGENTS.md:184-186).


Part 18

Deepen-weave F: main-loop comparison with OpenManus / nanobot (reinforces Chapters 6/12)

Step OpenManus nanobot Kortix/Suna
Receive message stdin Channel → MessageBus Webhook/CLI/SDK/useSession → session-lifecycle
Assemble context Memory list hard-truncated system+memory+history repo md + env + OpenCode transcript + manifest
Call model LLM.ask_tool AgentRunner + in-house provider OpenCode → LLM Gateway
Call tools in-process ToolCollection in-process tools/MCP sandbox local tools + server-side Executor
End terminate tool model stops or policy user stop / sandbox reclaim; persistence via CR
Permissions almost open tool-level + UNTRUSTED IAM + grant + keep secrets + CR + Review
Multi-tenant none one person, many channels account/project/session/sandbox four layers
Frontend almost none WebUI SPA Next Command Center + Desktop/Mobile

Paste this table next to Chapter 6 and you will stop asking “why is there no ReAct in the API.”


Part 19

Deepen-weave G: beginner debugging checklist (practical)

When you feel “the Agent is not responding,” debug by layer:

  1. API health: curl localhost:8008/v1/health (AGENTS.md:193)
  2. Session row: is project_sessions.status provisioning / running / failed?
  3. Sandbox row: is session_sandboxes.status active? is there an external_id? (primary key should equal session id)
  4. Proxy: does /v1/p/<external>/8000/kortix/health show daemon=ok and opencode=ok?
  5. OpenCode id: is opencode_session_id empty? On Web look at useSession(...).opencodeSessionId
  6. Delivery: do API logs show prompt_async non-ok / deliver deadline (engine.ts:687-695; deliver.ts)
  7. Web billing gate: is noPlan blocking enabled? (page.tsx:83-97)
  8. Permissions: is the connector blocked by policy? Does Review Center have a pending approval?
  9. CR: the Agent said it fixed it but main did not change — did it only commit without kortix cr open / without merge?
  10. Manifest: did kortix ship / validate get blocked by @kortix/manifest-schema?

Part 20

Deepen-weave H: inventory still explicitly not found after full checkout

Mark the boundary honestly, so OpenCode upstream capabilities are not counted into this repo:

Item Status
In-repo ReAct think/act loop not found (in OpenCode)
Dream / automatic company-brain compaction service not found (MANIFESTO vision)
Platform-level unified prompt-cache service not found
/review production API none (mock only)
Registry deterministic install engine removed (changed to agent import)
OpenCode upstream auto-compact implementation details not in this repo; no assertion


Part 21

Writing notes

  • Analysis baseline (full-checkout deepened edition): commit 5862e95671cfd198c6a94ceb9c1fefe579f00a8b; version treats VERSION=0.10.14 as canonical (README badge 0.9.98 inconsistency already marked).
  • Verification method: critical paths were opened in source and checked; apps/web, all packages/*, desktop/mobile are included. Full OpenCode upstream source is still not in this repo; related conclusions are marked not found or “execution is inside OpenCode.”
  • Relative to the old sparse edition: delete every “apps/web not checked out” phrasing; weave command center / useSession mount, CR front/back full path, snapshot builder / providers, and the four new packages into the main chapters, rather than stacking them in an appendix.
  • Product contrast: comparisons with OpenManus / nanobot / Claude Code are based on public positioning and architectural differences; they do not assert closed-source implementation details.

End of lecture.

↑