spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1<!--2KHAELOR3File: docs/research/OPENCODE_ANALYSIS.md4Author: Simon-Pierre Boucher5Contact: contact@spboucher.ai6-->78# OpenCode — Deep Architecture Analysis (Phase 0 Research)910**Reference:** `references/opencode` — snapshot at commit `0bff28de09105088ff5bdefab91413d55c28dff1` (2026-08-09), repo `github.com/anomalyco/opencode`, version `1.18.x`.1112**Method:** real code-path tracing (imports, agent loop, tool registry, render tree, permission checks, persistence), not README reading. All paths are relative to the OpenCode repo root. Key symbols are cited as evidence.1314**Important context:** this snapshot is the *post-rewrite* OpenCode. The old Go/bubbletea TUI is gone; the entire product is now **TypeScript on Bun**, with a SolidJS-driven terminal renderer (`@opentui/*`). The codebase also visibly contains **two coexisting generations**: the live "v1" runtime (`packages/opencode/src/**`, schemas in `packages/core/src/v1/**`) and an in-progress event-sourced "v2" layer (`packages/core/src/{session,agent,permission,config,snapshot,tool}`). Both write to the same SQLite database. This mid-migration state is itself an architectural lesson (see "What OpenCode does poorly").1516---1718## 1. Monorepo and technology stack1920- Bun workspace monorepo (`package.json` at root, `packageManager: bun@1.3.14`, turbo for typecheck, oxlint). ~30 packages under `packages/`: the ones that matter for a terminal agent are:21 - `packages/opencode` — the CLI + engine (agent loop, tools, sessions, server routes).22 - `packages/core` — shared services: database, event system, filesystem, ripgrep, permission/agent/config schemas (v1 + v2), snapshots, shell, PTY.23 - `packages/tui` — the terminal UI (SolidJS + `@opentui/solid`).24 - `packages/sdk` — `openapi.json` + generated JS client (`packages/sdk/js/src/gen/{client.gen.ts,sdk.gen.ts,types.gen.ts}`).25 - `packages/schema` — Effect Schema definitions shared by everything (`session.ts` v1, `session-message.ts` v2, `permission`, `revert`, identifiers).26 - Plus web/app/desktop/console/enterprise packages that are irrelevant to the terminal product but bloat the repo.27- Core libraries: **Effect 4** (services, layers, streams, structured concurrency) everywhere in the engine; **SolidJS 1.9** + **`@opentui/core|solid|keymap` 0.4.5** for the TUI; **`ai` SDK 6** for provider streaming; **drizzle-orm + SQLite** for persistence; **web-tree-sitter** (bash/powershell WASM grammars) for command parsing; **fuzzysort** for fuzzy matching; **ripgrep** (bundled binary service, `packages/core/src/ripgrep`).28- The engine is written in "Effect service" style: every subsystem is a `Context.Service` with a `Layer` and explicit dependency list (e.g. `SessionProcessor.node = LayerNode.make({ service, layer, deps: [Session.node, Config.node, Snapshot.node, ...] })` in `packages/opencode/src/session/processor.ts:699`). This gives explicit wiring and testability at the cost of a steep idiom (generators, `Effect.gen`, `Deferred`, `PubSub` everywhere).2930---3132## 2. Client/server architecture3334### 2.1 Process model — one process, virtual HTTP3536The headline finding: **OpenCode is architected as client/server but usually runs as a single process.**3738- The server is an **Effect HttpApi** app (not raw hono in this generation): `packages/opencode/src/server/server.ts` builds `HttpApiApp.webHandler()` and exposes both `fetch(request)` and an **in-memory `request(input, init)`** entry (`Server.Default`, `server.ts:57-66`). `openapi()` derives the public OpenAPI document from `PublicApi`.39- The default `opencode` command (`packages/opencode/src/cli/cmd/tui.ts`, `TuiThreadCommand`) does **not** open a socket. It spawns the engine in a **Bun Worker** (`packages/opencode/src/cli/tui/worker.ts`) and gives the TUI:40 - `createWorkerFetch(client)` — a `fetch` implementation that serializes `{url, method, headers, body}` over an RPC channel; the worker replays it against `Server.Default().app.fetch(request)` (`worker.ts:31-49`). The base URL is a placeholder (`http://opencode.internal`).41 - `createEventSource(client)` — subscribes to `"global.event"` RPC events instead of SSE (`cli/cmd/tui.ts:42-49`); the worker forwards every `GlobalBus` event over RPC (`worker.ts:24-26`).42- The same TUI can run against a **real** HTTP server: `packages/cli/src/tui.ts` (`runTui({url, headers})`) uses plain HTTP + SSE with exponential backoff. `opencode serve` / `opencode attach` / `opencode web` (`packages/opencode/src/cli/cmd/{serve,attach,web}.ts`) expose the same API over the network; `server.ts` supports mDNS advertisement (`MDNS`) and CORS.4344**Consequence:** the client is written 100% against the SDK/API surface, but the common path pays no network tax and needs no daemon lifecycle management. UI ↔ engine isolation also means a render-thread stall never blocks tool execution and vice versa.4546### 2.2 API surface and SDK4748- Route groups: `packages/opencode/src/server/routes/instance/httpapi/groups/{session,permission,question,provider,config,file,event,project,workspace,pty,mcp,tui,global,...}.ts`.49- The JS SDK is **generated from `packages/sdk/openapi.json` with `@hey-api/openapi-ts`** (`packages/sdk/js/script/build.ts`; ~162 paths / 188 operations into `client.gen.ts` / `sdk.gen.ts` / `types.gen.ts`), so client and server cannot drift silently. The TUI consumes it via `createOpencodeClient({baseUrl, fetch, directory, headers})` (`packages/tui/src/context/sdk.tsx`). A `packages/sdk-next` variant embeds the engine in-process for SDK consumers.50- Events reach clients through SSE handlers — per-instance `/event` and server-wide `/global/event` (`server/routes/instance/httpapi/handlers/{event,global}.ts`): `text/event-stream`, listener registered into an unbounded queue *before* the stream body starts ("Listener registration is eager, so events published after this point cannot be lost"), filtered by instance directory/workspace, prefixed with `server.connected`, merged with a 10s `server.heartbeat`. Response gzip middleware explicitly bypasses SSE paths.51- Multiple clients (TUI + web + IDE + ACP) can attach to one engine; the server is **multi-tenant across project directories via an `x-opencode-directory` header**, with per-directory engine state held in a `ScopedCache` keyed on `ctx.directory` (`packages/opencode/src/effect/instance-state.ts`) and instance boot deduplicated through `Deferred`s (`packages/opencode/src/project/instance-store.ts`). The share feature and control-plane routes build on the same event stream.5253### 2.3 Event system — the engine's spine5455Three layers (all evidence in `packages/opencode/src/bus/global.ts`, `packages/core/src/event.ts`, `packages/opencode/src/event-v2-bridge.ts`):56571. **`GlobalBus`** — a plain in-memory `EventEmitter` for process-level fan-out to SSE/RPC clients.582. **`EventV2`** — typed pub/sub on Effect `PubSub`, with a critical property: event definitions can be **durable** (`options: {durable: {aggregate: "sessionID", version: 1}}` in `packages/schema/src/v1/session.ts`). Publishing a durable event transactionally appends it to the `event` table (per-aggregate `seq` from `event_sequence`) **and runs registered projectors in the same transaction** ("Local operational projection committed atomically with a new durable event").593. **`EventV2Bridge`** — attaches location info (directory/workspace/project) to every publish and re-emits onto `GlobalBus`, including a second `sync` envelope for durable events (replication/share).6061**Persistence is a projection of events.** `packages/core/src/session/projector.ts` maps `SessionV1.Event.{Created,Updated,MessageUpdated,PartUpdated,...}` onto the `session` / `message` / `part` tables; session cost and token counters are maintained *incrementally* (`sql\`${col} + ${delta}\`` applied from `step-finish` parts, with negative deltas on part replacement/removal). `packages/opencode/src/session/session.ts` `updateMessage`/`updatePart` never touch the DB directly — they only publish events. The UI, the DB, and remote clients are all consumers of the same stream. This is the single most elegant structural decision in the codebase.6263---6465## 3. TUI framework6667### 3.1 Stack and rendering model6869- **SolidJS 1.9.10 (patched) + `@opentui/core` + `@opentui/solid` + `@opentui/keymap`** — opentui is the team's own terminal UI engine: a **retained-mode renderable tree with Yoga flexbox layout painted into an optimized cell buffer at a target FPS**, driven declaratively through Solid's fine-grained reactivity. JSX intrinsics are terminal renderables: `<box>`, `<text>`, `<span>`, `<scrollbox>`, `<textarea>`, `<input>`, `<markdown>`, `<code>`, `<diff>`, `<spinner>`.70- Bootstrap: `packages/tui/src/app.tsx:191-213` — `createCliRenderer({ targetFps: 60, useKittyKeyboard: {}, exitOnCtrlC: false, externalOutputMode: "passthrough", useMouse: ... })` inside an Effect `acquireRelease`; `render(() => <Providers…/>, renderer)` from `@opentui/solid`.71- Rendering is dirty-driven, not full-redraw: components call `renderer.requestRender()` only for imperative fixups; normal updates flow from Solid signals into individual renderables. Custom cell-level painting is possible (`FrameBufferRenderable` subclass with `renderSelf(buffer: OptimizedBuffer)` in `packages/tui/src/component/bg-pulse.tsx`).72- **No web tech, no marked, no shiki in the TUI**: markdown is a native opentui renderable (`<markdown streaming={true} internalBlockMode="top-level" …/>`), and syntax highlighting is **tree-sitter WASM grammars + nvim-treesitter highlight queries** (35+ languages declared in `packages/tui/src/parsers-config.ts`, registered via `addDefaultParsers`). Highlight styles are generated from the active theme (`generateSyntax(theme)` in `packages/tui/src/theme/index.ts`).7374### 3.2 Component architecture7576- Routing is a plain Solid store, not a router library: `packages/tui/src/context/route.tsx` (`Route = HomeRoute | SessionRoute | PluginRoute`, `navigate()` via `reconcile`). Top-level `<Switch>` in `app.tsx:1112` → `<Home/>` / `<Session/>`; the session subtree remounts keyed on session id.77- ~25 nested context providers compose the app (`app.tsx:247-349`): Exit → ErrorBoundary → Keymap → SDK → Sync → Theme → Local → Dialog → Frecency → PromptHistory → …78- The session view (`packages/tui/src/routes/session/index.tsx`, 2725 lines) renders messages with a plain `<For>` inside an opentui `<scrollbox stickyScroll stickyStart="bottom">`. **There is no list virtualization**; instead the sync store hard-caps at **100 messages per session** and GCs older parts (`packages/tui/src/context/sync.tsx:341-358`). Bounding the data instead of virtualizing the view is a deliberate simplification.79- Tool parts dispatch through `PART_MAPPING = { text: TextPart, tool: ToolPart, reasoning: ReasoningPart }` and a `toolDisplay()` switch to per-tool components (`Shell`, `Edit`, `Read`, `Task`, …), built on two presentation primitives: `InlineTool` (one line, collapsed) and `BlockTool` (bordered block).80- The sidebar (`routes/session/sidebar.tsx`, fixed width 42, auto-shown when width > 120) is composed entirely of plugin slots — even first-party UI (todos, LSP, MCP, changed files) is a "feature plugin" under `packages/tui/src/feature-plugins/`.8182### 3.3 State sync — server state mirrored into a Solid store8384`packages/tui/src/context/sync.tsx` is the heart of the client:8586- One `createStore` holding `session`, `message`, `part`, `permission`, `question`, `todo`, `agent`, `provider`, `config`, `lsp`, `mcp`, `vcs`, … Sessions/messages/parts are **sorted arrays maintained with binary search** (`search()` helper) so each event application is O(log n) + splice.87- Streaming text arrives as **`message.part.delta` events**, appended in place with `produce()` — Solid's granular reactivity then repaints only the affected `<markdown>` node. This is the entire streaming render path; there is no diffing of whole messages.88- SSE events are **coalesced in a 16 ms window and applied inside Solid's `batch()`** ("Batch all event emissions so all store updates result in a single render", `packages/tui/src/context/sdk.tsx:48-80`).89- Three-phase bootstrap (`loading` → `partial` → `complete`): a small blocking fetch set (providers, agents, config, project), then everything else non-blocking. Sessions hydrate lazily (`session.sync(sessionID)` fetches info + last 100 messages + todos + diff), with an explicit race guard so a slower REST snapshot never clobbers fresher SSE-streamed text (`hydratingSessions`; regression tests `test/cli/cmd/tui/sync-live-hydration.test.tsx`).9091### 3.4 Keyboard system9293- `@opentui/keymap` wrapped by `packages/tui/src/keymap.tsx`. Bindings are declared *reactively and locally* via `useBindings(() => ({ mode, commands, bindings }))` in whatever component owns them; command metadata (`{name, title, category, slashName, slashAliases, suggested, run}`) doubles as the source for both the command palette and slash commands.94- **Leader key** (`<leader>` = `ctrl+x`, 2 s timeout, `registerTimedLeader`), key aliases, and a **mode stack** (`base` / `modal` / `autocomplete`): dialogs push `"modal"` mode, which automatically suppresses all `base`-mode layers — that is how overlays capture input without manual focus bookkeeping.95- ~230 rebindable actions in `packages/tui/src/config/keybind.ts` (`Definitions` with defaults + descriptions), validated by Effect Schema, configured in a **separate `tui.json`** (deliberately split from `opencode.json`). A "which-key" overlay (`feature-plugins/system/which-key.tsx`) shows pending key sequences.9697### 3.5 The composer (prompt)9899`packages/tui/src/component/prompt/index.tsx` (1716 lines) — the single most engineered component:100101- One `<textarea>` renderable, min height 1, max height `max(6, height/3)`, multiline via `shift+return`/`ctrl+j`, with syntax-styled text and configurable cursor.102- **Extmarks** (editor-style virtual text spans) are the core mechanism: file mentions, agent mentions, and collapsed pastes are inserted as `input.extmarks.create({start, end, virtual: true, styleId})`, mapped to structured `PromptInfo.parts` via `extmarkToPartIndex`; `syncExtmarksWithPromptParts()` rederives part offsets from live extmark positions on every edit. The submitted prompt is thus **structured parts** (text + file refs + agent refs), not a flat string.103- **`@` file mentions**: width-aware trigger detection (`Intl.Segmenter` + `Bun.stringWidth` so offsets match terminal cells, `packages/tui/src/prompt/display.ts`), server-side fuzzy file finding (`sdk.client.v2.fs.find`, ranked server-side and deliberately not re-sorted), line-range syntax `@file#12-40`, agent and MCP-resource completion in the same popup.104- **Frecency**: JSONL at `<state>/frecency.jsonl`, score `frequency / (1 + ageDays)`, folded into autocomplete ranking (`score * (1 + frecencyScore)`).105- **Slash commands** are just palette commands with a `slashName`, merged with server-defined commands, ranked by fuzzysort with an exact-prefix bonus.106- **Paste intelligence**: bracketed paste decoding; ≥3 lines or >150 chars collapses to a `[Pasted ~N lines]` extmark expanded only at submit; path-looking pastes become file attachments; images/PDFs become base64 file parts.107- **Shell mode**: `!` at offset 0 switches the composer into shell mode; submit routes to `session.shell` instead of the agent.108- History (`<state>/prompt-history.jsonl`, 50 entries, cursor-position-aware navigation), stash (named drafts), external `$EDITOR` round-trip that re-locates extmark placeholders afterwards, IME-safe submit (double `setTimeout` flush), and an explicit double-Enter race guard with a regression test (`test/cli/tui/prompt-submit-race.test.ts`).109110### 3.6 Dialogs, palette, model selector111112- `packages/tui/src/ui/dialog.tsx`: a dialog stack rendered as an absolutely-positioned `zIndex={3000}` layer with an alpha scrim; focus is saved on open and restored only if the previous renderable is still mounted. Escape/ctrl+c dismissal is disabled while a mouse text-selection is active — a small detail that protects copy behavior.113- `packages/tui/src/ui/dialog-select.tsx` (791 lines) is the generic list dialog: categories, fuzzysort filter, footer key hints, action buttons, mouse + keyboard. Every picker (model, agent, session, theme, workspace…) is a thin wrapper over it.114- Command palette (`component/command-palette.tsx`, `ctrl+p`) reads reachable commands *from the keymap itself* (`keymap.getCommandEntries({namespace: "palette", visibility: "reachable"})`) and shows their live bindings — one registry powers keys, palette, and slash commands.115- Model selector (`component/dialog-model.tsx`): Favorites / Recent categories, provider ordering, deprecated filtering, sub-dialogs for provider and variant.116117### 3.7 Terminal capabilities, theming, resize118119- Terminal palette is queried (`renderer.getPalette({size: 16})`) and a **"system" theme is synthesized from the actual terminal colors** (`generateSystem(colors, mode)`); dark/light is detected via `waitForThemeMode` plus a raw DEC escape sniffer for live OS theme switches. 33 bundled themes as JSON assets; custom themes from `~/.config/opencode/themes/` and `.opencode/themes/` with SIGUSR2 hot reload.120- Kitty keyboard protocol enabled; mouse optional; win32 gets FFI-level console-mode fixes (`packages/tui/src/terminal-win32.ts`, `dlopen("kernel32.dll")`); `ctrl+z` suspend/resume handled properly (`renderer.suspend()` + `SIGCONT`).121- Resize: everything derives from `useTerminalDimensions()` memos (`wide() = width > 120` toggles sidebar and split-vs-unified diffs). `externalOutputMode: "passthrough"` keeps stray `console.log` from corrupting frames.122- Diffs render through opentui's native `<diff>` renderable (split/unified, syntax highlighting, wrap modes, full theme color set), used in the edit tool view, permission previews, and a full-screen diff viewer feature-plugin (`feature-plugins/system/diff-viewer.tsx`, 1077 lines: file tree, hunk navigation `]`/`[`, git/branch/last-turn modes).123124---125126## 4. Agent architecture127128### 4.1 Agents = permission policies (mostly), not prompts129130`packages/opencode/src/agent/agent.ts` defines `Agent.Info = {name, mode: "subagent"|"primary"|"all", permission: Ruleset, model?, prompt?, temperature?, steps?, hidden?, …}`.131132The decisive finding: **`build` and `plan` share the same system prompt and the same tool registry — they differ only in permission rulesets** (plus reminder injection):133134- `build`: defaults + `{question: "allow", plan_enter: "allow"}`.135- `plan`: defaults + `{question: "allow", plan_exit: "allow", edit: {"*": "deny", ".opencode/plans/*.md": "allow"}, task: {general: "deny"}}` (`agent.ts:157-181`). Plan mode's edit-blocking is *pure permission policy*; `Permission.disabled()` also derives tool visibility from the ruleset (a tool whose last matching rule is `pattern:"*", action:"deny"` is removed from the model's tool list), so `write`/`edit`/`apply_patch` disappear in plan mode rather than erroring.136- Plan behavior is reinforced by **synthetic reminder parts** appended to the last user message (`packages/opencode/src/session/reminders.ts` injecting `session/prompt/plan.txt`, `plan-mode.txt`, `build-switch.txt`) — not by a different system prompt.137- Neither built-in primary agent has a `prompt` field; the system prompt is selected by **model family**: `SystemPrompt.provider(model)` in `packages/opencode/src/session/system.ts` picks `session/prompt/anthropic.txt` for Claude, `gpt.txt`/`gemini.txt`/etc. otherwise. An agent `prompt` field *replaces* this entirely (`session/llm/request.ts:60`).138- Utility agents (`explore`, `compaction`, `title`, `summary`) are hidden agents with their own prompts and `"*": "deny"` + allow-lists — e.g. `explore` is a read-only search subagent (`agent/prompt/explore.txt`).139- Custom agents: markdown files in `.opencode/agent{s}/**/*.md` with YAML frontmatter (`ConfigAgent.load`, schema `ConfigAgentV1.Info`) or JSON config; a permissive YAML fallback exists "because other coding agents like claude code allow invalid yaml".140- Mid-session agent switching is trivial because **agent is a per-message field** (`SessionV1.User.agent`), cycled with Tab in the TUI. `plan_exit` is a tool that asks the user a question and then simply writes a new user message with `agent: "build"` (`packages/opencode/src/tool/plan.ts`).141142### 4.2 The agent loop — state-machine over persisted messages143144The loop lives in `packages/opencode/src/session/prompt.ts` (`SessionPrompt.runLoop`, lines 1081-1341), with per-turn stream handling in `packages/opencode/src/session/processor.ts` (`SessionProcessor`). Shape:145146```147while (true) {148 msgs = MessageV2.filterCompactedEffect(sessionID) // re-read persisted state149 {lastUser, lastAssistant, tasks} = MessageV2.latest(msgs)150 if (assistant finished && no pending tool calls) break // exit condition derived from state151 task = tasks.pop()152 if (task is "subtask") { handleSubtask(...); continue }153 if (task is "compaction") { compaction.process(...); continue }154 if (lastFinished overflows) { compaction.create(...); continue }155 msg = new assistant message156 handle = processor.create({assistantMessage, sessionID, model})157 tools = SessionTools.resolve({agent, session, model, ...})158 system = [environment, instructions, mcp, skills]159 result = handle.process({system, messages, tools, model}) // one streamed LLM step160 if (result === "stop") break161 if (result === "compact") compaction.create(...)162}163```164165Key properties, all verified in source:166167- **The database is the loop's state.** Each iteration re-derives what to do from persisted messages/parts; compaction and subagent invocations are *persisted parts* (`compaction`, `subtask` part types) popped as tasks. A crashed process can resume mid-conversation because nothing lives only in loop-local variables. `SessionPrompt.loop` wraps `runLoop` in `state.ensureRunning(...)` so concurrent prompts join the running loop instead of double-driving it.168- **`SessionProcessor` is a pure stream-event reducer** (`processor.ts:278-537`): a `handleEvent` switch over `text-start/delta/end`, `reasoning-*`, `tool-input-*`, `tool-call`, `tool-result`, `tool-error`, `step-start/finish`, `finish`. Every event immediately becomes a part upsert (`session.updatePart`) or a delta event (`session.updatePartDelta`) — persistence and UI streaming are the same operation.169- **Doom-loop detection**: if the last 3 parts are the same tool with byte-identical JSON input, a `doom_loop` permission request interrupts the run (`processor.ts:353-380`, `DOOM_LOOP_THRESHOLD = 3`).170- **Snapshots bracket every step**: `snapshot.track()` before the stream and at `step-start`/`step-finish`; a `patch` part with `{hash, files}` is recorded whenever files changed (`processor.ts:424-470`) — this powers revert and the diff viewer.171- **Interrupts are first-class**: `Effect.onInterrupt` marks the assistant message aborted; `cleanup()` waits up to 250 ms for in-flight tool calls, then marks stragglers `status: "error", metadata.interrupted: true`; the message-to-model converter later turns pending/running tool parts into `"[Tool execution was interrupted]"` results so Anthropic never sees a dangling `tool_use` (`message-v2.ts toModelMessagesEffect`).172- **Retry is a policy around the stream** (`SessionRetry.policy`, surfaced as a live `retry` status with attempt count), and provider `content-filter` finishes are converted into visible errors instead of silent idles (`prompt.ts:1301-1308`).173- Usage/cost come from real provider metadata at `step-finish` (`Session.getUsage`), incrementally accumulated onto the assistant message and session row.174175### 4.3 Subagents176177`packages/opencode/src/tool/task.ts`:178179- `task` spawns a **child session** (`sessions.create({parentID, agent, permission})`) — fresh context by design; `task_id` lets the model resume a prior child session. The parent's model/variant is inherited unless the subagent pins its own.180- **Permission derivation** (`agent/subagent-permissions.ts`): only the parent's *deny* rules and `external_directory` rules propagate to the child; the subagent's own ruleset defines its capabilities. `task` and `todowrite` are force-denied for children unless explicitly granted — combined with `subagent_depth` (default 1, checked by walking `parentID`), this prevents recursive agent explosions.181- Background subagents (behind a flag): results are injected back into the parent session as synthetic `<task id=… state=…>` text parts; a `BackgroundJob` service manages wait/promotion/cancel.182- Rendering: the parent's TUI shows child-session progress live because child events flow over the same bus.183184---185186## 5. State and persistence187188### 5.1 Storage backend189190- **SQLite + drizzle, WAL mode**, at `~/.local/share/opencode/opencode.db` (`packages/core/src/database/database.ts`; pragmas: WAL, `synchronous=NORMAL`, `busy_timeout=5000`, 64 MB cache). Legacy JSON-file storage (`packages/opencode/src/storage/storage.ts`) survives only for `session_diff`.191- Tables (`packages/core/src/session/sql.ts` + `database/schema.gen.ts`): `session` (typed columns: project/workspace/parent ids, directory, title, cost, token counters, `revert` json, `permission` json, agent, model), `message` and `part` (**ids + timestamps as columns, payload as one JSON blob** — schema-flexible, index-poor by design), `todo`, `event`/`event_sequence` (durable event log), `permission` (persisted approvals, v2), `project`, `project_directory`, `workspace`.192- IDs are monotonic ULID-like strings (`msg…`, `prt…`, hex-time prefix + counter, `packages/schema/src/identifier.ts`) so `ORDER BY id` equals insertion order — this quietly simplifies pagination, part ordering, and merge logic everywhere.193- Reads use keyset pagination (`MessageV2.page()`, cursor = base64 `{id, time}`) and batched part hydration.194195### 5.2 Message model196197`packages/schema/src/v1/session.ts`: `Info = User | Assistant` (role-discriminated); `Part` is a 12-variant union: `text`, `reasoning`, `file`, `tool`, `step-start`, `step-finish`, `snapshot`, `patch`, `agent`, `subtask`, `retry`, `compaction`. Tool state is a status-discriminated union `pending → running → completed | error` with `time{start,end,compacted?}` — `time.compacted` marks a completed tool result whose output was pruned. Assistant carries `parentID` (its user message), `cost`, `tokens{input,output,reasoning,cache{read,write}}`, `finish`, `error` (a typed union of named errors: `AbortedError | APIError | AuthError | ContextOverflowError | OutputLengthError | ContentFilterError | UnknownError`).198199The v2 model (`packages/schema/src/session-message.ts`) flattens this to a message union (`User | Assistant | Shell | Compaction | AgentSwitched | ModelSwitched | …`) with assistant content inline — evidence the team found message+parts too granular in practice.200201### 5.3 Snapshots and revert — the git shadow repository202203`packages/opencode/src/snapshot/index.ts` is one of OpenCode's best ideas:204205- A **separate git dir** per project/worktree at `~/.local/share/opencode/snapshot/<projectID>/<hash(worktree)>`, operated as `git --git-dir <shadow> --work-tree <real>`. The user's repo is never touched — no commits, no index changes, invisible to `git status`.206- Performance: the shadow repo's `objects/info/alternates` points at the real repo's object DB and the real index is *copied* on seed — "on huge repos like chromium … `git add --all` rebuilding the hashes can take minutes. By doing this we eliminate this at all."207- `track()` = `git add --all` (candidates computed from `diff-files` + untracked, ignoring >2 MiB untracked files) + `git write-tree` → a **tree hash** stored in `step-start`/`step-finish`/`patch` parts.208- `restore(hash)` = `read-tree` + `checkout-index -a -f`; `revert(patches)` = per-file `git checkout <hash> -- <file>` with existence-aware deletion.209- Session revert (`packages/opencode/src/session/revert.ts`): pick a message boundary → restore files from accumulated patch parts → session marked reverted but messages intact (fully undoable via `unrevert`) → only when the user prompts *past* the revert are trailing messages destructively removed.210211### 5.4 Configuration212213- `opencode.json[c]` global (`~/.config/opencode/`) + project (walk-up from cwd to worktree, nearest wins), plus `.opencode/` dirs, env (`OPENCODE_CONFIG`, `OPENCODE_CONFIG_CONTENT`, `OPENCODE_PERMISSION`), remote/org/MDM layers — ten merge stages in `packages/opencode/src/config/config.ts` (deep-merge via remeda, arrays for `instructions` set-unioned).214- `{env:VAR}` and `{file:path}` substitution in any config value (`config/variable.ts`). Everything validated by Effect Schema (not zod). Writes preserve JSONC formatting via `jsonc-parser` edits; `$schema` auto-injected.215- TUI concerns (keybinds, theme) are deliberately **quarantined in `tui.json`** — engine config stays client-agnostic.216- Instructions files: `AGENTS.md` (plus `CLAUDE.md` compatibility unless disabled) — global first-hit, then **first match walking up** from cwd to worktree ("The first project-level match wins so we don't stack AGENTS.md/CLAUDE.md from every ancestor"), plus `config.instructions` globs/URLs (`packages/opencode/src/session/instruction.ts`). Two clever behaviors: (1) **lazy injection** — reading a file under a directory with its own AGENTS.md appends that file's instructions to the tool output as a `<system-reminder>`, deduped per assistant message; (2) the v2 `SystemContext` layer diffs instruction changes mid-session and issues explicit "these instructions replace…" deltas.217218---219220## 6. Tools221222### 6.1 Design philosophy: few tools, tiny schemas, rich outputs223224The core registry (`packages/opencode/src/tool/registry.ts`) wires: `bash(shell)`, `read`, `write`, `edit`, `apply_patch`, `grep`, `glob`, `task`, `todowrite`, `question`, `skill`, `webfetch`, `websearch`, `lsp`, `plan_exit` (+ MCP/plugin tools). Schemas are deliberately minimal — evidence:225226- `edit`: **4 parameters** (`filePath`, `oldString`, `newString`, `replaceAll?`) — `tool/edit.ts:47-56`.227- `read`: 3 (`filePath`, `offset?`, `limit?`); `grep`: 3 (`pattern`, `path?`, `include?`); `glob`: 2; `write`: 2; `bash`: 3 (`command`, `timeout?`, `workdir?` — "Use this instead of 'cd' commands", `tool/shell/prompt.ts`).228- Long guidance lives in the **description text files** (`edit.txt`, `read.txt`, `shell.txt`, …), not in parameter complexity. Descriptions can even vary by shell (PowerShell vs bash chaining notes are templated into `shell.txt`).229- `Tool.define` (`tool/tool.ts`) wraps every tool with: schema decode → typed `InvalidArgumentsError` whose message is model-facing repair prose ("Please rewrite the input so it satisfies the expected schema"), automatic **output truncation with file spill** (`Truncate.output` — oversized output goes to `~/.local/share/opencode/tool-output/` and the model is told to Read/Grep it), and tracing spans.230- Tool context (`Tool.Context`) exposes `ask()` (permission), `metadata()` (streamed progress metadata for the UI), `abort` signal, and the session messages — tools are UI-aware without owning rendering.231232### 6.2 The edit tool — nine-stage replacer cascade233234`packages/opencode/src/tool/edit.ts` (approaches credited in-file to Cline and gemini-cli). `replace()` (line 682) runs a generator-based `Replacer` cascade, first match wins:2352361. `SimpleReplacer` — exact string.2372. `LineTrimmedReplacer` — line-by-line comparison with trimmed whitespace.2383. `BlockAnchorReplacer` — first/last lines as anchors (≥3 lines), middle lines matched by Levenshtein similarity ≥ 0.65, block-size tolerance ±25%, best-of-multiple-candidates.2394. `WhitespaceNormalizedReplacer` — all whitespace collapsed.2405. `IndentationFlexibleReplacer` — common leading indentation removed.2416. `EscapeNormalizedReplacer` — unescapes `\n`, `\t`, `\"`… (LLMs over-escape).2427. `TrimmedBoundaryReplacer` — trimmed-boundary match.2438. `ContextAwareReplacer` — anchor lines + ≥50% middle-line match.2449. `MultiOccurrenceReplacer` — all exact occurrences (for `replaceAll`).245246Guards: uniqueness required unless `replaceAll` (`index !== lastIndex → continue`); `isDisproportionateMatch()` refuses fuzzy matches much larger than `oldString` ("Re-read the file and provide the full exact oldString"); distinct error messages for *not found* vs *ambiguous*. Around the replacement: CRLF detection and preservation (`detectLineEnding`/`convertToLineEnding`), BOM preservation (`Bom.split/join/syncFile`), per-file semaphore locks, atomic-ish write + auto-format hook (`Format.Service`), a unified diff computed and attached to both permission request metadata and tool metadata, and **LSP diagnostics appended to the tool output** ("LSP errors detected in this file, please fix: …") — the model gets type errors in the same turn as the edit.247248### 6.3 read / grep / glob / write / bash249250- `read` (`tool/read.ts`): 2000-line default, 50 KB byte cap, per-line 2000-char truncation, streaming line reader that stops at the cap; binary sniffing (extension list + non-printable ratio on a 4 KB sample); images/PDFs returned as real attachments; directories readable by the same tool (entry listing); **"Did you mean" suggestions on miss** (`miss()` lists up to 3 near-name files); output wrapped in `<path>/<type>/<content>` tags with `(Showing lines X–Y of Z. Use offset=N to continue.)` continuation hints; background LSP warm-up on read.251- `grep`/`glob` (`tool/grep.ts`, `tool/glob.ts`): thin wrappers over a bundled **ripgrep** service, hard limit 100 results, structured concise output with explicit truncation notices ("Consider using a more specific path or pattern") — never dumps thousands of lines into context.252- `write` (`tool/write.ts`): full-file write; diff computed against existing content for the permission request; BOM/format preservation; LSP diagnostics for the file *and* up to 5 other affected files.253- `bash` (`tool/shell.ts`, 645 lines): commands are **parsed with tree-sitter** (bash + PowerShell WASM grammars) before execution — for permission patterns (below) and for detecting filesystem-verb arguments escaping the workspace (`external_directory` checks). Cross-shell support is real (bash/zsh/PowerShell 5/7/cmd with per-shell prompt guidance). Output beyond line/byte limits spills to a file with instructions to Read/Grep it.254255### 6.4 What KHAELOR's spec calls `process`256257OpenCode does not expose a first-class `process.start/list/read/write/stop` tool to the model. It has: a PTY subsystem (`packages/core/src/pty`, server routes `groups/pty.ts`) used by clients (desktop/web terminals), background jobs for subagents (`packages/opencode/src/background/job.ts`), and shell-mode/`!` for user-run commands. Long-running dev servers under *model* control are a gap — KHAELOR's planned `process` tool goes beyond OpenCode here.258259---260261## 7. Context management262263- **Budget:** `usable()` = model input limit minus a reserved compaction buffer (`COMPACTION_BUFFER = 20_000` or configured `compaction.reserved`), `isOverflow()` compares real usage (input+output+cache tokens from provider metadata) against it (`packages/opencode/src/session/overflow.ts`).264- **Compaction is part of the loop, not a side process:** when a step finishes in overflow, the processor sets `needsCompaction`, the stream is cut (`Stream.takeUntil`), and the loop persists a `compaction` task part; the next iteration runs `compaction.process()` — a hidden `compaction` agent generates a summary assistant message (`summary: true`) with its own prompt (`agent/prompt/compaction.txt`); subsequent context building reads `filterCompactedEffect` (messages after the last summary + the summary itself). A configurable recent tail is preserved verbatim (25% of usable, clamped 2k–8k tokens, `MIN/MAX_PRESERVE_RECENT_TOKENS` in `session/compaction.ts`).265- **Pruning is a second, cheaper mechanism:** `compaction.prune()` walks backwards protecting the newest `PRUNE_PROTECT = 40_000` tokens of tool outputs, then blanks older tool outputs (marking `time.compacted`, rendered to the model as `"[Old tool result content cleared]"`) if at least `PRUNE_MINIMUM = 20_000` tokens are reclaimable; `skill` outputs are never pruned. Old tool noise disappears without paying an LLM summarization pass.266- **System prompt assembly** per step (`prompt.ts:1257-1269`): environment info, instruction files, MCP instructions, skills — all as separate system blocks; the model-family prompt from `SystemPrompt.provider()`.267- **Prompt caching:** `packages/opencode/src/provider/transform.ts` applies `cacheControl: {type: "ephemeral"}` breakpoints for Anthropic-family models (`transform.ts:362-380`) — cache hits/writes then show up in the real token accounting.268269---270271## 8. Permission system and UX272273### 8.1 Model274275- Rules are `{permission, pattern, action}` triples; **evaluation = last matching rule wins** with wildcard matching on both fields; unmatched default is `ask` (`packages/opencode/src/permission/index.ts`: `evaluate()` is a 4-line `findLast`). `merge()` is array concatenation — later rulesets override by position. Config key order is preserved (`propertyOrder: "original"`) so users control precedence by ordering.276- Permission keys are *capability-ish tool families*: `read, edit, bash, task, external_directory, webfetch, websearch, question, doom_loop, skill, todowrite, glob, grep, lsp` — note `write`/`apply_patch` map onto **`edit`**, and MCP resource tools onto `read`, so policy is written against capabilities rather than tool names.277- Sensible defaults (`Agent.fromConfig` defaults in `agent/agent.ts:119-136`): `"*": "allow"` but `read: {"*.env": "ask", "*.env.example": "allow"}`, `external_directory: {"*": "ask"}`, `doom_loop: "ask"`, `question: "deny"` (enabled per-agent).278279### 8.2 Bash gets special treatment — tree-sitter + arity280281`tool/shell.ts` parses the command AST and issues a permission request whose `patterns` are the **exact command texts** and whose `always` suggestions come from `BashArity.prefix()` (`permission/arity.ts`) — a generated dictionary of how many tokens constitute a meaningful command prefix (`git: 2`, `git config: 3`, `npm run: 3`, `docker compose: 3`…). So running `git push origin main` offers "always allow `git push *`" — precise, human-meaningful generalization instead of all-or-nothing. Filesystem verbs (`rm/cp/mv/…` + PowerShell/cmd equivalents) additionally trigger `external_directory` checks when arguments resolve outside the workspace.282283### 8.3 Flow and UX284285- Tools call `ctx.ask({permission, patterns, always, metadata})`; the service evaluates against `merge(agent.permission, session.permission)`; `deny` throws immediately (typed `DeniedError`), `allow` passes, `ask` parks a `Deferred` and publishes `permission.asked` over the bus (`permission/index.ts:67-107`).286- The TUI renders an inline panel in the session view (`packages/tui/src/routes/session/permission.tsx`), with **metadata-driven bodies** — an edit permission shows the actual diff (via `<diff>`), bash shows the command, `doom_loop` gets special copy. Replies: once / always / reject; rejecting in a subagent context opens a feedback textarea whose text is delivered to the model as a typed `CorrectedError` ("the user said no, and here is why") — rejection becomes steering, not a dead end.287- `always` approvals are **session-scoped in-memory** in v1 (per docs: "for the rest of the current OpenCode session"); durable per-project persistence exists only in the v2 `permission` table (`packages/core/src/permission/saved.ts`). Granting `always` auto-resolves other pending requests that now evaluate to allow; rejecting one rejects all pending requests in the session.288- Deny rules also *shape the tool list* (`Permission.disabled`), and non-interactive `opencode run` injects denies for `question`/`plan_enter`/`plan_exit`.289290---291292## 9. Performance engineering293294What makes OpenCode feel fast even when inference is slow — all verified:2952961. **Delta-only streaming end to end**: provider delta → `updatePartDelta` event → SSE/RPC → `produce()` append in the Solid store → single `<markdown>` node repaint. No message re-render, no layout thrash.2972. **16 ms event coalescing + `batch()`** at the client boundary (`context/sdk.tsx`) — one render per frame regardless of event rate.2983. **Sorted arrays + binary search** for store updates; `reconcile`/`produce` keep Solid subscriptions stable.2994. **Bounded session data** (100-message cap with part GC) instead of virtualization complexity.3005. **In-process worker RPC** instead of sockets for the default path; UI and engine on separate threads.3016. **SQLite/WAL with incremental counters** — cost/token totals maintained by delta at part-write time, never recomputed by scanning.3027. **Shadow-git snapshots with object alternates + copied index** — checkpointing is near-free even on huge repos.3038. **ripgrep for all search**, hard result limits everywhere, tool-output spill-to-file with model-side pagination.3049. **Prompt-cache breakpoints** for Anthropic (ephemeral cacheControl) plus a **session-scoped `promptCacheKey`** (`provider/transform.ts`) — real latency/cost reduction on every step. The models.dev catalog is disk-cached with a cross-process flock and **inlined into the compiled binary at build time** (`OPENCODE_MODELS_DEV` define) so startup never blocks on the network.30510. TUI micro-craft: theme `SyntaxStyle` destruction deferred to `renderer.idle()`; palette prewarm before first paint to avoid theme flash; startup loader appears only after 500 ms (then holds ≥3 s to avoid flicker); FPS dropped 60→30 while a decorative animation is mounted; autocomplete returns the previous options while loading to avoid list flicker; spinner degrades to a static glyph when animations are disabled.30611. **Startup discipline**: the CLI ships as a `bun build --compile` binary (`OPENCODE_WORKER_PATH` compiled in, bunfig/dotenv autoload disabled), with a **codified lazy-import rule** (root `AGENTS.md:64`) and `lazy()` memo helpers (`packages/opencode/src/util/lazy.ts`) keeping heavy modules (provider SDK packages — 23 bundled as lazy thunks, others `npm install`ed at runtime with `ignoreScripts: true` — tree-sitter, LSP) off the cold-start path. The TUI itself uses no dynamic `import()`; "lazy" there means lazy *data* (per-session hydration, `createResource`), not lazy modules.307308Known warts, admitted in-source: several `setTimeout(0) + markDirty() + requestRender()` sequences in the composer labeled "workaround… needs to be addressed properly" (`component/prompt/index.tsx:241-247, 1217-1221`).309310---311312## 10. Testing and quality signals313314- Regression tests exist precisely where races live: `test/cli/tui/prompt-submit-race.test.ts` (double-Enter phantom prompt), `test/cli/cmd/tui/sync-live-hydration.test.tsx` (SSE vs REST hydration race), `test/permission/arity.test.ts`, `test/agent/plan-mode-subagent-bypass.test.ts` (plan-mode subagents can still edit).315- Comments frequently encode *why*: dropped orphan reasoning deltas, ConPTY paste normalization, IME flush timing, Windows console modes via FFI. The codebase reads like several years of accumulated terminal-reality scar tissue — this is exactly the knowledge KHAELOR should mine.316317---318319## WHAT OPENCODE DOES VERY WELL3203211. **Event-sourced session state.** Durable typed events + atomic projectors (`packages/core/src/event.ts`, `session/projector.ts`) make the DB, the TUI, remote clients, and share/replay all consumers of one stream. Persistence and streaming are literally the same write.3222. **A loop that derives its next action from persisted state.** `SessionPrompt.runLoop` re-reads messages each iteration; compaction and subtasks are persisted parts, exits are derived from message state — crash-safe, resumable, and steerable by construction.3233. **Permissions as data, evaluated in four lines.** `findLast(wildcard-match)` over rule arrays, capability-style keys (`write` folds into `edit`), tool visibility derived from the same rules, and *agents/modes as permission policies* rather than parallel agent implementations.3244. **The bash permission bridge.** Tree-sitter parsing + the arity dictionary turn "always allow" into precise, human-meaningful patterns (`git push *`), and catch filesystem escapes (`external_directory`) before execution.3255. **The edit replacer cascade** — nine matching strategies with uniqueness and disproportionate-match guards, CRLF/BOM preservation, diff in the permission prompt, and LSP diagnostics fed back in the tool result. Model-facing error messages are written as repair instructions.3266. **Shadow-git snapshots.** Zero-pollution checkpointing of the working tree (alternates + copied index for O(1) seeding on huge repos), powering per-step patch parts, revert/unrevert, and the diff viewer.3277. **Streaming render economics.** Delta events → Solid fine-grained updates → single-node repaints, with 16 ms coalescing and `batch()`. The TUI stays at 60 fps during full-speed token streams.3288. **Composer engineering.** Extmark-backed structured prompt parts (mentions, collapsed pastes), frecency-ranked server-side file search, cursor-aware history, stash, external-editor round-trip, IME and paste correctness. This is the best terminal input widget in any agent surveyed.3299. **One command registry** feeding keybindings, command palette, and slash commands, with a mode stack that makes dialog input capture automatic.33010. **Tool schema minimalism with rich prose.** 2–4 parameters per tool; guidance in description text; oversized output spilled to files the model can Read/Grep; explicit truncation/continuation hints in every output.33111. **Terminal-reality hardening**: kitty keyboard, terminal-palette-derived "system" theme, live dark/light detection, win32 FFI console fixes, suspend/resume, selection-safe dialogs, copy-on-select.332333## WHAT OPENCODE DOES POORLY3343351. **Two coexisting architectures.** v1 and v2 session/permission/agent/config systems live side by side (`packages/core/src/v1/**` vs `packages/core/src/{session,permission,...}`), with bridges (`EventV2Bridge`, `event-v2-bridge.ts`) and duplicated schemas. Every subsystem must be read twice to know what is live. A migration is understandable; shipping both indefinitely is architectural debt.3362. **Sheer scale and surface area.** ~30 packages, web/desktop/console/enterprise/slack/stats alongside the CLI; the session route component is 2725 lines; config merging has ten layers including remote org configs and MDM. The essential terminal agent is maybe 20% of the repo.3373. **Effect as a hard prerequisite.** Effect 4 (beta) generators, layers, `Deferred`, `PubSub` pervade everything. It buys real structured concurrency, but the abstraction tax is high and it pins the project to a fast-moving beta dependency (plus a patched solid-js, patched effect, 15+ patched deps in root `package.json`).3384. **"Always allow" is not durable in the live path** — v1 approvals are in-memory per session; durable persistence exists only in the not-yet-primary v2 tables. Users re-approve across restarts.3395. **No model-facing process manager.** PTY/background-job infrastructure exists, but the model cannot start/inspect/stop long-running processes as a first-class tool — dev-server workflows degrade to blocking bash or user-side shell mode.3406. **Message+parts granularity backfired.** The v2 schema flattening (message unions with inline content) is implicit admission that the v1 part explosion (12 part types, part-level events, part GC in the client) cost more than it returned.3417. **Provider-generality tax everywhere.** Model-family prompt switching (`anthropic.txt`, `gpt.txt`, `beast.txt`…), per-provider transforms, patched AI-SDK forks — necessary for their business, but it spreads conditional complexity through the model layer.3428. **Some UI state is fragile by admission** — setTimeout-based layout workarounds in the composer, a 50 ms polling interval to anchor the autocomplete popup, dead prompt files (`plan-reminder-anthropic.txt` unreferenced).3439. **No virtualized history**: the 100-message cap is pragmatic but means long sessions silently drop scrollback from the UI (data remains in SQLite, but the user can't scroll to it).344345## WHAT KHAELOR SHOULD ADOPT3463471. **The event-sourced spine, simplified.** Typed events as the single source of truth; persistence as an atomic projection of durable events; UI and session store fed from the same stream. This directly implements KHAELOR's §7 typed event bus and makes replay/resume free. Use SQLite/WAL with incremental usage counters and ULID-style monotonic IDs.3482. **State-derived agent loop.** Kernel iterations re-derive the next action from persisted session state; compaction and steering are enqueued as persisted items, not in-memory control flow. Keep the stream reducer (`SessionProcessor` equivalent) separate from the loop (`runLoop` equivalent) — that *is* the small kernel.3493. **The edit replacer cascade wholesale** (it is MIT, sourced from Cline/gemini-cli lineage): all nine strategies, the uniqueness and disproportionate-match guards, CRLF/BOM handling, and model-facing repair-prose errors. Add LSP-style diagnostics feedback later.3504. **Tool design discipline**: ≤4 parameters, guidance in descriptions, output caps with spill-to-file + Read/Grep continuation, `<path>/<content>` structured outputs, "did you mean" on file miss, `workdir` parameter instead of `cd`.3515. **Bash permission parsing**: tree-sitter command extraction + an arity table for "always allow `git push *`" suggestions, and external-directory detection on filesystem verbs. This is the difference between a permission system users tolerate and one they like.3526. **Modes as permission policies + reminder injection**, not separate agents: KHAELOR's capability policies (§13) can express plan/build exactly as OpenCode does, including deriving tool visibility from deny rules. Agent-as-message-field makes mid-session switching trivial.3537. **Shadow-git snapshots** for baseline protection (§16: "record baseline state before edits") and per-step diffs — including the alternates + copied-index seeding trick.3548. **Composer techniques**: extmark-style structured prompt parts, collapsed paste placeholders expanded at submit, frecency-ranked file mentions, cursor-position-aware history, stash. Also the single command registry powering keys + palette + slash commands, and the keymap mode stack for dialogs.3559. **Client performance recipe**: delta-only part updates, ~16 ms event coalescing into batched store updates, previous-result retention during async filtering, palette prewarm before first paint, deferred destruction of styling resources.35610. **Interruption semantics**: abort marks the assistant message; bounded grace for in-flight tools; interrupted tool calls converted to synthetic error results so the Anthropic conversation never contains dangling `tool_use` blocks; rejection-with-feedback (`CorrectedError`) turning permission denials into steering.35711. **Doom-loop detection** (3 identical consecutive tool calls → ask the user) — cheap, effective, honest.35812. **In-process "server"**: even Anthropic-only and single-client, keeping the kernel behind an internal typed API/event boundary (as OpenCode does with worker RPC) preserves KHAELOR's clean layering without daemon complexity.359360## WHAT KHAELOR SHOULD NOT COPY3613621. **The dual v1/v2 architecture.** Design the event/message schema once, version it from day one (`{durable, version}` on event definitions is worth copying), and never ship two parallel session systems.3632. **The Effect framework dependency.** KHAELOR needs structured concurrency, cancellation, and typed services — achievable with plain TypeScript (AbortController trees, small service containers, async iterators) without pinning the whole codebase to a beta ecosystem and a fleet of patched packages. Adopt the *patterns* (layered services, explicit deps, deferred completion), not the framework.3643. **Multi-provider scaffolding**: model-family prompt files, provider transform matrices, AI-SDK indirection. KHAELOR V1 is Anthropic-only behind a single `ModelClient` — one prompt, one streaming protocol, native prompt-caching and thinking support.3654. **The monorepo sprawl** (web/desktop/console/enterprise/plugins/slots). KHAELOR is one CLI. Even OpenCode's TUI plugin-slot system, elegant as it is, is premature for V1.3665. **12-part message granularity with part-level GC.** Follow the direction of OpenCode's own v2 correction: fewer, flatter message shapes; keep tool state as one status-discriminated object.3676. **The 100-message UI cap as the only scrollback strategy** — KHAELOR should either virtualize or page older history into view on demand rather than silently truncating.3687. **In-memory-only "always allow"** — persist scoped approvals (project-scoped, pattern-based) from V1, as OpenCode's v2 tables belatedly do.3698. **Ten-layer config merging** (remote org configs, MDM, well-known endpoints). KHAELOR's hierarchy is four layers (CLI → project → user → env → defaults); keep it that way.3709. **setTimeout-based layout patching and polling anchors** in the composer — budget real fixes for editor/layout interaction rather than accreting workarounds.37110. **Skipping a model-facing process manager.** OpenCode's biggest tool-level gap is KHAELOR's planned differentiator (`process.start/list/read/write/stop`) — do not inherit the omission.372