Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [6.9.7] - 2026-10-06

### Added

- **Effort selector for GLM 5.3, GLM 5.3 Flash, Gemini 3.8 Flash and DeepSeek V4.1 Flash.** `/effort` opens a Faster ↔ Smarter slider (low · medium · high · max) for the current model: ←/→ to adjust, Enter to save, `s` to apply it to this session only. `/effort <level>` sets it directly, ←/→ in `/model` adjusts the highlighted model's effort, and the status bar shows the level in effect. The default is Medium. Picks are saved per model in `~/.orbcode/config.json` and read on every request, so every chat on the machine (new, resumed or already running) uses them until changed. OrbCode sends the level as `X-MATTERAI-REASONING-EFFORT`; the backend maps it to what the serving gateway accepts (e.g. GLM has no medium tier, so it runs at high; Fireworks tops out at high). Which models get the selector comes from the catalog's `reasoning_efforts`, with a built-in fallback for older backends.
- **Confirm before resuming a session with a cold prompt cache.** The gateway keeps a conversation's prompt cache for 5 minutes; after that, the first resumed turn re-reads the whole context at the uncached input price. `/resume` and `orbcode --resume <id>` now show how long the session has been idle, how large it is, and what resuming will cost as a share of your weekly usage limit (monthly on Lite, dollars for your own provider keys). The share comes from the backend's new `GET /axoncode/usage/estimate`, which prices the request exactly like a real one; if it can't be reached, the prompt shows the context size without a cost figure. You can then resume, or start a new conversation and pull the old one in later with `/task`. The prompt appears for any cold session with 100k+ tokens of context (the share fills in once the backend answers), and for smaller ones when the cost is at least 1% of the window; estimates are prefetched while the `/resume` picker is open so picking a session doesn't wait on the network. It's skipped when a prompt is passed alongside `--resume <id>`.

### Fixed

- **Compaction no longer leaves a conversation stuck.**
- A "context too long" error from the provider now compacts and retries the step instead of failing the turn; before, every following turn hit the same error.
- A history that has already outgrown the window (e.g. a session resumed on a smaller-window model) still compacts: the summary request is trimmed to fit (older tool results stubbed, oldest turns dropped), with smaller budgets tried if the provider's real window is tighter than the catalog's.
- The 80% check now counts tool output and new input added since the last usage report, not just the last reported size.
- A failed auto-compaction is retried on the next turn instead of staying off for the rest of the session.
- A user message that triggers compaction as a turn starts is kept verbatim after the summary.
- **`read_file` caps characters as well as lines** (100k per file, 200k per call), so a minified bundle or a file of very long lines can't flood the context in one read.
- **Your model and its effort now survive a restart.** Models that only exist in the backend catalog (e.g. DeepSeek V4.1 Flash) weren't known until `/v1/models` answered, so on startup the saved model fell back to GLM 5.3 Flash and its effort to medium. The last fetched catalog is now cached in `~/.orbcode/models-cache.json` and loaded before settings, the saved model is restored once the live catalog arrives, and the plan-default logic reads the saved model from disk instead of the fallback.
- **A replaced conversation no longer leaks into the next one.** `/new`, `/resume`, sign-in/out and MCP reloads now abort the in-flight turn and drop any events, approval requests or follow-up questions it emits while unwinding, so a turn that was still running can't stream into the new conversation's transcript.

## [6.9.6] - 2026-10-06

### Added
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,13 @@ Then, from any project directory:
orbcode
```

To update later: `npm update -g @matterailab/orbcode` (or re-run the install command).
Updates install in the background: a new release (once it has been public for
24h) is downloaded into `~/.orbcode/versions/`, verified, and used from your
next launch — the banner just says to restart. If a new version fails to start,
OrbCode falls back to the previous one. To opt out, set
`ORBCODE_DISABLE_AUTOUPDATE=1` or `"autoUpdates": false` in
`~/.orbcode/settings.json`; you can always update manually with
`orbcode update` or `npm update -g @matterailab/orbcode`.

### From source (development)

Expand Down
13 changes: 13 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ Source (`src/`), tests, and workflow files are not published.
or broken `dist/`. The CLI reads its version from `package.json` at runtime —
bumping the version is all that's needed; no source change required.

## Background updates — compatibility rules

Installed CLIs stage new releases under `~/.orbcode/versions/` and roll back to
the previous version if a release fails to start (see `src/utils/autoUpdate.ts`
and `bin/select-version.js`). Releases auto-install only after 24h on npm, so
**ship a fix within a day to keep a bad release from spreading**, and keep these
invariants so old and new versions can run against the same `~/.orbcode`:

- `config.json`, `settings.json` and session files: additive changes only —
never rename, remove or change the type of an existing field.
- `bin/orbcode.js` must keep accepting `--version` (prints the bare version)
and `--self-test` (prints `ok <version>`); updates are verified with them.

## One-time setup

1. **Create an npm Automation token** (npmjs.com → Access Tokens →
Expand Down
67 changes: 51 additions & 16 deletions bin/orbcode.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,27 @@
#!/usr/bin/env node
import { spawn } from "node:child_process"
import { readFileSync } from "node:fs"
import { createRequire } from "node:module"
import { fileURLToPath } from "node:url"

process.title = "orbcode"

const args = process.argv.slice(2)
const first = args[0]
const versionOrHelpOnly =
args.includes("--help") || args.includes("-h") || args.includes("--version") || args.includes("-v")
const runsWithoutTui =
args.includes("--help") ||
args.includes("-h") ||
args.includes("--version") ||
args.includes("-v") ||
versionOrHelpOnly ||
args.includes("--print") ||
args.includes("-p") ||
first === "update" ||
first === "mcp" ||
first === "plugin" ||
first === "plugins"

if (!runsWithoutTui && !process.versions.bun) {
const entrypoint = fileURLToPath(new URL("../dist/index.js", import.meta.url))
let bunExecutable
try {
bunExecutable = createRequire(import.meta.url).resolve("bun/bin/bun.exe")
} catch {
console.error("OrbCode's bundled UI runtime is missing. Reinstall @matterailab/orbcode and try again.")
process.exit(1)
}
const child = spawn(bunExecutable, [entrypoint, ...args], { stdio: "inherit" })
/** Run a child with inherited stdio, forwarding termination signals to it. */
async function runChild(command, childArgs, env) {
const child = spawn(command, childArgs, { stdio: "inherit", env })
const forwardedSignals = ["SIGINT", "SIGTERM", "SIGHUP"]
const forwarders = new Map()
for (const signal of forwardedSignals) {
Expand All @@ -41,12 +34,54 @@ if (!runsWithoutTui && !process.versions.bun) {
child.once("exit", (code, signal) => resolve({ code, signal }))
})
for (const [signal, forward] of forwarders) process.off(signal, forward)
return result
}

function exitLike(result) {
if (result.signal) process.kill(process.pid, result.signal)
process.exit(result.code ?? 1)
}

// Hand off to a newer version staged by the background updater, if any. Any
// problem selecting or starting it falls through to running this copy.
if (!process.versions.bun) {
let staged = null
try {
const { selectStagedVersion } = await import("./select-version.js")
const selfVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
staged = selectStagedVersion({ selfVersion, countAttempt: !versionOrHelpOnly })
} catch {
staged = null
}
if (staged) {
const result = await runChild(process.execPath, [staged.bin, ...args], {
...process.env,
ORBCODE_DELEGATED: "1",
})
if (!result.error) exitLike(result)
}
}

if (!runsWithoutTui && !process.versions.bun) {
const entrypoint = fileURLToPath(new URL("../dist/index.js", import.meta.url))
let bunExecutable
try {
bunExecutable = createRequire(import.meta.url).resolve("bun/bin/bun.exe")
} catch {
console.error("OrbCode's bundled UI runtime is missing. Reinstall @matterailab/orbcode and try again.")
process.exit(1)
}
// The background updater verifies new versions through this same Node
// launcher, so it needs to know which Node binary started us.
const result = await runChild(bunExecutable, [entrypoint, ...args], {
...process.env,
ORBCODE_NODE_PATH: process.env.ORBCODE_NODE_PATH || process.execPath,
})
if (result.error) {
console.error(`Unable to launch OrbCode's UI runtime: ${result.error.message}`)
process.exit(1)
}
if (result.signal) process.kill(process.pid, result.signal)
process.exit(result.code ?? 1)
exitLike(result)
}

await import("../dist/index.js")
97 changes: 97 additions & 0 deletions bin/select-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Picks which OrbCode install should handle this launch: the copy this file
// ships in, or a newer version the background updater staged under
// ~/.orbcode/versions/<version>/. Every failure path returns null, which means
// "run the current copy" — exactly the behaviour before background updates.
//
// Plain JS with no dependencies: it runs before anything else, under Node.
import * as fs from "node:fs"
import * as os from "node:os"
import * as path from "node:path"

const PACKAGE_PATH = ["node_modules", "@matterailab", "orbcode"]
const VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/

export const HEALTHY_MARKER = ".healthy"
export const ATTEMPTS_MARKER = ".attempts"
/** Launches that may fail to reach a healthy state before a version is skipped. */
export const MAX_UNHEALTHY_LAUNCHES = 1

export function getVersionsDir(env = process.env) {
const configDir = env.MATTERAI_CONFIG_DIR || path.join(os.homedir(), ".orbcode")
return path.join(configDir, "versions")
}

export function getVersionBin(versionDir) {
return path.join(versionDir, ...PACKAGE_PATH, "bin", "orbcode.js")
}

export function isVersionString(value) {
return typeof value === "string" && VERSION_RE.test(value)
}

/** -1 / 0 / 1, matching src/utils/updateCheck.ts#compareVersions. */
export function compareVersions(a, b) {
const split = (v) => {
const [main, pre = ""] = v.split("-", 2)
const parts = main.split(".").map((p) => {
const n = Number.parseInt(p, 10)
return Number.isFinite(n) ? n : 0
})
while (parts.length < 3) parts.push(0)
return [parts, pre]
}
const [aMain, aPre] = split(a)
const [bMain, bPre] = split(b)
for (let i = 0; i < 3; i++) {
if (aMain[i] !== bMain[i]) return aMain[i] < bMain[i] ? -1 : 1
}
if (aPre === bPre) return 0
if (aPre === "") return 1
if (bPre === "") return -1
return aPre < bPre ? -1 : 1
}

export function readAttempts(versionDir) {
try {
const n = Number.parseInt(fs.readFileSync(path.join(versionDir, ATTEMPTS_MARKER), "utf8"), 10)
return Number.isFinite(n) && n > 0 ? n : 0
} catch {
return 0
}
}

export function isHealthy(versionDir) {
return fs.existsSync(path.join(versionDir, HEALTHY_MARKER))
}

/** A staged version that failed to start and never proved itself healthy. */
export function isBad(versionDir) {
return !isHealthy(versionDir) && readAttempts(versionDir) >= MAX_UNHEALTHY_LAUNCHES
}

/**
* Returns `{ version, bin }` for a staged version to delegate to, or null to
* run the current copy. `countAttempt` is false for launches that never reach
* a health check (--version / --help) so they can't mark a version bad.
*/
export function selectStagedVersion({ selfVersion, env = process.env, countAttempt = true }) {
try {
if (env.ORBCODE_DELEGATED) return null
if (!isVersionString(selfVersion)) return null
const versionsDir = getVersionsDir(env)
const current = fs.readFileSync(path.join(versionsDir, "current"), "utf8").trim()
if (!isVersionString(current)) return null
if (compareVersions(current, selfVersion) <= 0) return null
const versionDir = path.join(versionsDir, current)
const bin = getVersionBin(versionDir)
if (!fs.existsSync(bin)) return null
if (!isHealthy(versionDir)) {
const attempts = readAttempts(versionDir)
if (attempts >= MAX_UNHEALTHY_LAUNCHES) return null
if (countAttempt) fs.writeFileSync(path.join(versionDir, ATTEMPTS_MARKER), String(attempts + 1))
}
return { version: current, bin }
} catch {
return null
}
}
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@matterailab/orbcode",
"version": "6.9.6",
"version": "6.9.7",
"description": "OrbCode CLI — agentic coding in your terminal, by MatterAI",
"type": "module",
"bin": {
Expand Down Expand Up @@ -42,13 +42,18 @@
"test:search": "node --import tsx --test test/search-files.test.ts",
"test:rewind": "node --import tsx --test test/rewind.test.ts",
"test:headless": "node --import tsx --test test/headless.test.ts",
"test:ui": "bun test test/ui-viewport.test.tsx test/session-picker.test.tsx test/rewind-picker.test.tsx",
"test:autoupdate": "node --import tsx --test test/auto-update.test.ts",
"test:warmup": "node --import tsx --test test/cache-warmup.test.ts",
"test:resume": "node --import tsx --test test/resume-cost.test.ts",
"test:compaction": "node --import tsx --test test/compaction.test.ts test/agent-context.test.ts test/files.test.ts",
"test:effort": "node --import tsx --test test/model-effort.test.ts test/model-persistence.test.ts",
"test:ui": "bun test test/ui-viewport.test.tsx test/session-picker.test.tsx test/rewind-picker.test.tsx test/resume-confirm.test.tsx test/model-picker.test.tsx test/effort-picker.test.tsx",
"prepublishOnly": "npm run typecheck && npm run build"
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.85",
"@ai-sdk/openai-compatible": "^2.0.51",
"@modelcontextprotocol/sdk": "^1.29.0",
"@modelcontextprotocol/sdk": "^1.32.1",
"@opentui/core": "0.4.3",
"@opentui/react": "0.4.3",
"ai": "^6.0.207",
Expand Down
Loading
Loading