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
9 changes: 7 additions & 2 deletions fork/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,11 @@
"marker": "fork: 5s was not enough headroom on a mu",
"note": "Fork edit to an upstream file (+11/-1 vs baseline). Marker is a string the fork's edit introduces and upstream does not contain, so a merge that reverts the edit fails fork:verify."
},
{
"file": "packages/core/src/installation/version.ts",
"marker": "InstallationReleaseChannels",
"note": "Fork edit to an upstream file. Adds InstallationIsRelease (channel is latest/beta/prod) alongside the existing InstallationLocal, shared by database.ts's session-db path and upgrade.ts's self-upgrade eligibility check so any non-release build — not just literally channel \"local\" — is treated as never having been published. Marker is a string the fork's edit introduces and upstream does not contain, so a merge that reverts the edit fails fork:verify."
},
{
"file": "packages/core/src/database/sqlite.bun.ts",
"marker": "fork: this DB file is shared by every co",
Expand Down Expand Up @@ -408,8 +413,8 @@
},
{
"file": "packages/opencode/src/cli/upgrade.ts",
"marker": "InstallationLocal",
"note": "Fork edit to an upstream file (+8/-1 vs baseline). Marker is a string the fork's edit introduces and upstream does not contain, so a merge that reverts the edit fails fork:verify."
"marker": "InstallationIsRelease",
"note": "Fork edit to an upstream file. Skips the self-upgrade check for any non-release channel (was only the literal \"local\" channel, which missed a local build on the fork's own \"dev\" channel — confirmed live: it read as needing a release patch and silently curl-installed over a running dev binary mid-session). Marker is a string the fork's edit introduces and upstream does not contain, so a merge that reverts the edit fails fork:verify."
},
{
"file": "packages/opencode/src/command/index.ts",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/database/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Global } from "../global"
import { Flag } from "../flag/flag"
import { isAbsolute, join } from "path"
import { DatabaseMigration } from "./migration"
import { InstallationChannel } from "../installation/version"
import { InstallationChannel, InstallationIsRelease } from "../installation/version"
import { LayerNode } from "../effect/layer-node"
import { makeGlobalNode } from "../effect/app-node"

Expand Down Expand Up @@ -57,7 +57,7 @@ export function path() {
return join(Global.Path.data, Flag.OPENCODE_DB)
}
if (
["latest", "beta", "prod"].includes(InstallationChannel) ||
InstallationIsRelease ||
process.env.OPENCODE_DISABLE_CHANNEL_DB === "1" ||
process.env.OPENCODE_DISABLE_CHANNEL_DB === "true"
)
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/installation/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@ declare global {
export const InstallationVersion = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"
export const InstallationChannel = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local"
export const InstallationLocal = InstallationChannel === "local"
// The only channels a real published release ever carries. Anything else —
// "local", "dev", or a one-off OPENCODE_CHANNEL override — is a build that
// was never uploaded anywhere, so comparing its version against the latest
// published release is meaningless and self-upgrading over it is actively
// harmful (it silently replaces a binary someone is deliberately running for
// its own changes, mid-session, with unrelated published code).
export const InstallationReleaseChannels = ["latest", "beta", "prod"]
export const InstallationIsRelease = InstallationReleaseChannels.includes(InstallationChannel)
20 changes: 13 additions & 7 deletions packages/opencode/src/cli/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@ import { Config } from "@/config/config"
import { AppRuntime } from "@/effect/app-runtime"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Installation } from "@/installation"
import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version"
import { InstallationIsRelease, InstallationVersion } from "@opencode-ai/core/installation/version"
import { GlobalBus } from "@/bus/global"

export async function upgrade() {
// A local dev build (working-tree dist, channel "local") must never be
// nudged toward a release build — 0.0.0-dev-* vs 1.x always reads as a
// major update. Only real installs (curl/brew/npm channels) check.
// Only a real published release (channel latest/beta/prod) has a
// meaningful version to compare against "the latest release" in the first
// place. A local or dev build — including a local build on the stable
// "dev" channel (see build-local.ts) — was never published, so the
// comparison is meaningless at best and actively harmful at worst: it can
// read as needing "a patch" and silently curl-install a real release over
// a binary someone is deliberately running for its own changes, mid-session
// (confirmed live 2026-09-19 — replaced a running dev build and corrupted
// the TUI's rendered screen with the installer's raw progress output).
// OPENCODE_ALWAYS_NOTIFY_UPDATE is the explicit test hook and overrides
// this: it forces the toast even on a dev build, for verifying the
// notification path without installing a release.
if (InstallationLocal && !Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE) return
// this: it forces the toast even on a non-release build, for verifying the
// notification path without installing anything.
if (!InstallationIsRelease && !Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE) return
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal()))
if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return
const method = await Installation.method()
Expand Down
Loading