Skip to content
 
 

Repository files navigation

oc: Custom Build of opencode

This repository is oc, a custom fork of opencode maintained for personal use. It tracks upstream opencode (currently based on the v1.18.27 tag) and is rebased onto new upstream releases as they land. There is no prebuilt binary and no published package: installing opencode from npm/bun gets the stock upstream build with none of these changes. The repo contains everything needed to build the patched version (full source + bun.lock; dist/ and node_modules/ are generated, not stored).

Build

Requires only bun:

git clone https://github.com/malventano/oc && cd oc
bun install
cd packages/opencode
OPENCODE_VERSION=1.18.27-oc bun run script/build.ts --single --skip-install --skip-embed-web-ui
cp dist/opencode-linux-x64/bin/oc /usr/local/bin/oc
  • bun install at the repo root is REQUIRED, and it applies the patchedDependencies: the build bundles the patched @opentui/core (patches/@opentui%2Fcore@0.4.5.patch), plus the other patched deps (solid-js, effect, ai-sdk providers, tanstack, etc.). The opentui patch is load-bearing - it carries the render fixes (0196 drawUnstyledText diff setter, 0338 the completion-flip raw sync gated to the void buffer - the white-flash fix on longer writes, 0201 scroll height-delta anchor). A build run with --skip-install WITHOUT a prior bun install would bundle STOCK opentui and silently lose those fixes. --skip-install is only a speedup for the artifact step; it skips the extra cross-platform bun install @opentui/core@... in build.ts, not the root bun install that applies the patch.
  • --skip-embed-web-ui is required: v1.18.5+ app Rollup cannot resolve @opencode-ai/client/promise (upstream dep issue, not ours); plain bun run build fails on it.
  • Changing an @opentui patch does not re-patch an existing install. Bun keys the patched store on the package version, not the patch content, so after updating an opentui patch you must rm -rf node_modules/.bun/@opentui+core* node_modules/.bun/@opentui+solid* node_modules/@opentui/core node_modules/@opentui/solid && bun install to force re-apply. Otherwise an old checkout keeps its old store while a fresh clone gets the new one - the classic "works here, fails on a fresh clone" split (2026-09-09 incident: stale hunk offsets mangle parser.worker.js, see bugs/BUG_OPENTUI_PATCH_OFFSETS.md).
  • Committed @opentui patches must have ZERO-offset hunks. Bun applies hunks at the literal declared line numbers, while git apply context-matches and tolerates offsets (reporting Hunk #N succeeded at M (offset K)) - so git apply --check passing does NOT mean bun will produce a valid file. Verify with git apply -v (fail on any offset/fuzz line) and node --check node_modules/.bun/@opentui+core@0.4.5*/node_modules/@opentui/core/parser.worker.js after bun install.
  • Keep the -oc suffix in OPENCODE_VERSION: the autoupdate-disable patch (and oc upgrade message) key off it. Any <tag>-oc version works; 1.18.27-oc matches this base (the build in this repo is 1.18.27-oc-0338).
  • OPENCODE_VERSION also pins the channel to latest, which keeps the session DB at the standard ~/.local/share/opencode/opencode.db (shared with stock opencode). Building WITHOUT it puts the branch name in the channel and the DB becomes opencode-<branch>.db (e.g. opencode-main.db): a separate empty database, so no existing sessions appear and new ones land in the wrong file.
  • The built binary is dist/opencode-linux-x64/bin/oc (named oc, unlike upstream's opencode).

What the custom patches fix / add

Build & identity

  • Built artifact named oc; CLI rebranded (OC logo, oc command name, oc-specific tips)
  • Auto-update disabled: custom builds never query the upstream release channel (no npm/GitHub fetch at startup, no auto-replace of the binary); oc upgrade prints "custom build, update from its source repo" and exits

Compaction & KV-cache efficiency

  • Inject-method compaction: the /compact summary turn runs as a normal turn with the summary prompt injected as the final user message, keeping the request chain byte-identical to the cached prefix; the summary turn hits ~98% of the KV prefix cache instead of a full miss
  • Tail retention: the last N full turns are kept verbatim post-compaction (tail_turns, default 2), with the retention budget no longer clamped (recent turns survive fully)
  • Reasoning part type preserved on model switch (keeps the prompt byte-identical → cache hit)
  • Mode reminders persisted; build-switch gated to the n-1 assistant turn (byte-identical prompts)

Variants & model control

  • Variant overrides retained per model, shared across agents/modes (set max in plan, it stays max after toggling to build); legacy agent-scoped keys migrated on use
  • Opening an old session no longer restores its stale model/variant; the model resolves to the agent config default
  • Variant cycle fixes: explicit-off treated as a real cycle position; current variant passed through manual /compact
  • vLLM thinking control via chat_template_kwargs + MTP-safe body flatten (excludes min_p/logit_bias, which speculative decoding rejects)
  • reasoning_tokens fallback for glm45-family parsers that omit completion_tokens_details.reasoning_tokens
  • maxOutputTokens: provider limit.output authoritative, hard 32K cap fallback removed

Editing (flat-JSON edit tool)

  • Upstream flat-JSON method (0323): {filePath, oldString, newString, replaceAll} exact string replacement. The hashline (0026-0124) and fence (0124-0322) grammars are retired - they drove 22-63% failure/adherence classes and a V4A contamination collapse (endpoint-measured; docs/EDIT_TOOL_EVALUATION_20260908.md)
  • Matching ladder (0131, 0301): byte-exact primary; tolerance tiers (line-trimmed, whitespace/indent/escape-normalized, block-anchor, context-aware) fall back; non-byte-exact applies echo the matched tier + line, and NEW indentation rebases onto the file region
  • Fail-loud, fail-cheap: unique oldString required (multiple matches are named), disproportionate matches refused, empty oldString rejected on existing files, no partial writes
  • Read-before-edit enforced; minimal-anchor + region-read guidance in the tool description (0335)

Built-in tooling (ships in the binary)

  • Time context (0027, 0119): every user message and tool output carries a UTC <system-reminder> stamp (ISO 8601, second precision, trailing Z - TZ-invariant since 0119); the env block declares the user's local date + offset (Today's date: Wed Aug 13 2026 (America/Kentucky/Monticello, UTC-04:00), epoch-delta'd on change); sessions-browse/skill-metadata emit UTC Z seconds
  • Loop guard (0028): dual-channel loop detection: repeated identical tool output or repeated user prompts abort with a diagnostic instead of looping
  • Sessions DB tools (0030): read-only session browsing, SQLite queries, and confirm-gated write management against the session database
  • Squash output (0031): replace a finished tool output with a short summary you write (depth-gated), keeping the context chain small
  • Skill metadata (0034, 0038): frontmatter name/description, line/char counts, sibling inventory, mtime, and description byte count for one or all skills; reads raw SKILL.md frontmatter; built-in skills guarded
  • tmux pane management: run/poll/keys/capture/wait lifecycle ops for long-running jobs in visible panes
  • Shell safety guards (0036, 0040, 0041): pkill -f and kill -9 / -KILL on $$ / $PPID are blocked in the bash tool (anchored to command position; covers semicolon-suffixed and sudo forms) because they hang the session
  • Bash file-op guard plugin (see plugins/README.md): the ONE non-bundled piece - a hook plugin (bash-file-op-guard.js) that nudges toward the native file tools and hard-rejects the rg -rn/-r n (--replace n) foot-gun. Install it (copy + config plugin entry + restart) or the guard is silently absent.

Prompt guidance (0029, 0039, 0044)

  • Tool prompts: question/read/write/webfetch ship refined guidance (exact JSON array shapes for the question tool, URL discipline for webfetch, output-budget chunking for write)
  • default.txt ships time-awareness and output-efficiency guidance; the shell prompt warns to never kill -9 $$ / $PPID; write.txt documents that heredocs/redirections bypass Read-before-Write and the same-turn read cache
  • The edit description opens with a type-first requirement for every op

TUI

  • Subagent costs aggregated into the sidebar spent total (with placeholder-session-ID guard)
  • Message pruning on prompt submit instead of during streaming (no viewport jumps while scrolled up)
  • Session directory filter: non-git sessions scoped by directory, not hierarchical path
  • Footbar: session-cwd directory label (home ~-abbreviated), session title, cost DFS, path-first order
  • Adaptive refresh cadence + reduced render lag
  • Footbar spacing/order polish
  • Footbar overflow fixed on narrow terminals (0032)
  • Session resume: agent restore no longer races the agent-list load (0035)

Session directory scoping & DB path repair

Sessions are scoped to the directory they were launched in: oc shows a session only when the current working directory matches its stored launch directory (git projects match hierarchically by path relative to the worktree; non-git sessions match by exact directory, the behavior of our session-directory-filter work, PR #31210 / patch 0004). Sessions launched in a different directory do not appear in the list.

The stored directory can also diverge from the original launch path: when a session's directory becomes part of a git repo (or a project is initialized there), the session row can be reassigned to that project, so it stops showing under the original context. If sessions are missing, restore the stored paths to the original launch directories.

The session database is SQLite at ~/.local/share/opencode/opencode.db. Inspect:

sqlite3 ~/.local/share/opencode/opencode.db \
  "SELECT id, title, directory, path, project_id FROM session ORDER BY time_updated DESC;"

Restore a session to its original launch directory:

sqlite3 ~/.local/share/opencode/opencode.db \
  "UPDATE session SET directory = '/original/launch/dir', path = substr('/original/launch/dir', 2), project_id = 'global' WHERE id = '<session-id>';"
  • path mirrors directory: for non-git sessions it is the launch directory relative to / with the leading slash stripped (e.g. directory = /home/user/projpath = home/user/proj). This is the value the session filter matches against, so it must stay in sync with directory; do NOT set it to NULL (NULL-path sessions never match the TUI filter)

  • project_id is 'global' for non-git directories. If a session was reassigned into a git project, restore it. Sessions genuinely launched inside a git repo keep their project-specific project_id and worktree-relative path; resetting those hides them from the repo's session list

  • If a whole tree moved (renamed parent), rewrite both columns in bulk:

    sqlite3 ~/.local/share/opencode/opencode.db
    "UPDATE session SET directory = REPLACE(directory, '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/old/parent', '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/new/parent'), path = REPLACE(path, 'old/parent', 'new/parent') WHERE directory LIKE '/old/parent%';"

oc does not need to be stopped: you can direct oc to run these queries itself (the session database is live SQLite and the session list re-reads it). If a session is currently open in the TUI, reopen it to refresh its view.

Optional: redo every session in the DB (regenerates the filter path from each session's stored directory, undoing any stale or munged path values):

sqlite3 ~/.local/share/opencode/opencode.db \
  "UPDATE session SET path = substr(directory, 2) WHERE project_id = 'global' AND directory != '/';"

This applies to all non-git sessions (project global): their path is the launch directory relative to / (leading slash stripped, e.g. directory = /home/user/proj becomes path = home/user/proj), which is what the session filter matches against. Run it after correcting directory values. It is also the backfill for legacy rows: sessions created before opencode 2026-04-28 (migration add_session_path added the column without backfill) have path = NULL and never match the filter; this command fills them in. Idempotent: already-correct paths are rewritten to the same value. Sessions started inside a git repo are intentionally NOT touched: they carry a project-specific project_id and a worktree-relative path, and resetting either would hide them from the repo's session list. Verify with:

sqlite3 ~/.local/share/opencode/opencode.db \
  "SELECT path, COUNT(*) FROM session WHERE project_id = 'global' GROUP BY path ORDER BY COUNT(*) DESC LIMIT 10;"

Everything is built in

The oc binary ships the full tooling: hashline editing, sessions DB tools, time context, loop guard, squash output, skill metadata, tmux pane management, and shell safety guards are compiled in. Nothing needs to be copied, configured, or installed.

ONE exception: the bash file-op guard is a hook plugin, not bundled (see plugins/README.md) - copy plugins/bash-file-op-guard.js into your opencode plugin directory, declare it in the config plugin array, and restart opencode.

Repository layout

This repo is a lean snapshot of the upstream opencode release (upstream-specific CI, dogfood config, community docs, and deploy tooling are stripped) plus the oc patch series. lean-base.sh regenerates the snapshot from a new upstream tag; main is a derived artifact and force-pushes are the normal update path.

About

Forked from anomalyco/opencode. oc: custom build of opencode (372 commits ahead of v1.18.31, lean snapshot base, rebased on upstream releases)

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages