Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4cc6f5e
fix: prevent LCM inline content labels from being treated as file paths
belisarius222 Feb 25, 2026
6be6194
fix: make inline LCM payloads first-class storage kinds
rabsef-bicrym Feb 25, 2026
235703e
feat: add lcm_read tool for retrieving stored LCM file content
rabsef-bicrym Feb 25, 2026
d928736
fix: prevent processor from re-storing lcm_read output back into LCM
rabsef-bicrym Feb 25, 2026
6b99ceb
chore: create pebbles for PR review fixes
rabsef-bicrym Feb 25, 2026
b9e12ed
Revert "refactor: replace upstream upgrade sources with voltropy inst…
rabsef-bicrym Feb 25, 2026
92ac39a
bump: track upgrade refactor restoration as volt-da4
rabsef-bicrym Feb 25, 2026
2d3a293
volt-156.1: DRY LCM internal tools constant in session TUI
rabsef-bicrym Feb 25, 2026
da5541b
bump: track LongMemEval benchmark as volt-pebble
rabsef-bicrym Feb 25, 2026
64d2f17
volt-156.4: remove legacy getLargeFileContent fallback
rabsef-bicrym Feb 25, 2026
3b50403
volt-156.5: make large_files check-constraint migration atomic
rabsef-bicrym Feb 26, 2026
0be0e1a
volt-156.5: skip redundant large_files check rewrites
rabsef-bicrym Feb 26, 2026
64383c7
volt-156.3: type metadata.lcm escape hatch between lcm_read and proce…
rabsef-bicrym Feb 26, 2026
1981723
volt-156.8: add storage_kind migration for tenant schemas
rabsef-bicrym Feb 26, 2026
57d2d88
volt-156.2: remove dead storageKind field from lcm_read metadata
rabsef-bicrym Feb 26, 2026
5ad77e0
volt-156.7: cap lcm_read max_bytes schema at 100MB
rabsef-bicrym Feb 26, 2026
2cb9210
volt-156.9: handle inline_binary reads explicitly
rabsef-bicrym Feb 26, 2026
4f68aab
fix: replace CJS require("crypto") with ESM import in lcm db
rabsef-bicrym Feb 26, 2026
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
1 change: 1 addition & 0 deletions .pebbles/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pebbles.db
3 changes: 3 additions & 0 deletions .pebbles/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"prefix": "volt"
}
46 changes: 46 additions & 0 deletions .pebbles/events.jsonl

Large diffs are not rendered by default.

Binary file added .pebbles/pebbles.db
Binary file not shown.
1 change: 1 addition & 0 deletions packages/voltcode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export namespace Agent {
lcm_describe: "allow",
lcm_expand: "allow",
lcm_grep: "allow",
lcm_read: "allow",
external_directory: {
[Truncate.DIR]: "allow",
[Truncate.GLOB]: "allow",
Expand Down
22 changes: 16 additions & 6 deletions packages/voltcode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import type { TasksTool } from "@/tool/tasks"
import type { QuestionTool } from "@/tool/question"
import type { LcmExpandTool } from "@/tool/lcm-expand"
import type { LcmGrepTool } from "@/tool/lcm-grep"
import type { LcmReadTool } from "@/tool/lcm-read"
import { useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
import { useSDK } from "@tui/context/sdk"
import { useCommandDialog } from "@tui/component/dialog-command"
Expand Down Expand Up @@ -87,6 +88,8 @@ import "opentui-spinner/solid"

addDefaultParsers(parsers.parsers)

const LCM_INTERNAL_TOOLS = ["lcm_expand", "lcm_grep", "lcm_read"]

class CustomSpeedScroll implements ScrollAcceleration {
constructor(private speed: number) {}

Expand Down Expand Up @@ -1557,9 +1560,6 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
const { theme } = useTheme()
const ctx = use()

// Internal LCM tools that are always hidden (not in dev mode)
const LCM_INTERNAL_TOOLS = ["lcm_expand", "lcm_grep"]

// Check if there are hidden tools with no visible content
const hasHiddenToolsOnly = createMemo(() => {
// Check if there are any tool parts
Expand All @@ -1582,6 +1582,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
.filter((p) => p.type === "tool")
.every((p) => {
const toolPart = p as ToolPart
// See LCM_INTERNAL_TOOLS definition above
// Internal LCM tools are always hidden when not in dev mode
if (!ctx.devMode() && LCM_INTERNAL_TOOLS.includes(toolPart.tool)) return true
// Other tools are hidden when showDetails=false and completed
Expand Down Expand Up @@ -1835,12 +1836,10 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess
const ctx = use()
const sync = useSync()

// Internal LCM tools that should only be visible in dev mode
const LCM_INTERNAL_TOOLS = ["lcm_expand", "lcm_grep"]

// Hide tool if showDetails is false and tool completed successfully
// Hide internal LCM tools (lcm_expand, lcm_grep) when not in dev mode
const shouldHide = createMemo(() => {
// See LCM_INTERNAL_TOOLS definition above
// Hide internal LCM tools when not in dev mode
if (!ctx.devMode() && LCM_INTERNAL_TOOLS.includes(props.part.tool)) return true
if (ctx.showDetails()) return false
Expand Down Expand Up @@ -1889,6 +1888,9 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess
<Match when={props.part.tool === "lcm_grep"}>
<LcmGrep {...toolprops} />
</Match>
<Match when={props.part.tool === "lcm_read"}>
<LcmRead {...toolprops} />
</Match>
<Match when={props.part.tool === "grep"}>
<Grep {...toolprops} />
</Match>
Expand Down Expand Up @@ -2238,6 +2240,14 @@ function LcmGrep(props: ToolProps<typeof LcmGrepTool>) {
)
}

function LcmRead(props: ToolProps<typeof LcmReadTool>) {
return (
<InlineTool icon="↥" pending="Reading LCM content..." complete={props.input.file_id} part={props.part}>
LCM read {props.input.file_id}
</InlineTool>
)
}

function Grep(props: ToolProps<typeof GrepTool>) {
return (
<InlineTool icon="✱" pending="Searching content..." complete={props.input.pattern} part={props.part}>
Expand Down
44 changes: 37 additions & 7 deletions packages/voltcode/src/cli/cmd/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,41 @@ export const UpgradeCommand = {
command: "upgrade [target]",
describe: "upgrade volt to the latest or a specific version",
builder: (yargs: Argv) => {
return yargs.positional("target", {
describe: "version to upgrade to, for ex '0.1.48' or 'v0.1.48'",
type: "string",
})
return yargs
.positional("target", {
describe: "version to upgrade to, for ex '0.1.48' or 'v0.1.48'",
type: "string",
})
.option("method", {
alias: "m",
describe: "installation method to use",
type: "string",
choices: ["curl", "npm", "pnpm", "bun", "brew", "choco", "scoop"],
})
},
handler: async (args: { target?: string }) => {
handler: async (args: { target?: string; method?: string }) => {
UI.empty()
UI.println(UI.logo(" "))
UI.empty()
prompts.intro("Upgrade")
const detectedMethod = await Installation.method()
const method = (args.method as Installation.Method) ?? detectedMethod
if (method === "unknown") {
prompts.log.error(`volt is installed to ${process.execPath} and may be managed by a package manager`)
const install = await prompts.select({
message: "Install anyways?",
options: [
{ label: "Yes", value: true },
{ label: "No", value: false },
],
initialValue: false,
})
if (!install) {
prompts.outro("Done")
return
}
}
prompts.log.info("Using method: " + method)
const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest()

if (Installation.VERSION === target) {
Expand All @@ -28,11 +53,16 @@ export const UpgradeCommand = {
prompts.log.info(`From ${Installation.VERSION} → ${target}`)
const spinner = prompts.spinner()
spinner.start("Upgrading...")
const err = await Installation.upgrade("curl", target).catch((err) => err)
const err = await Installation.upgrade(method, target).catch((err) => err)
if (err) {
spinner.stop("Upgrade failed", 1)
if (err instanceof Installation.UpgradeFailedError) {
prompts.log.error(err.data.stderr)
// necessary because choco only allows install/upgrade in elevated terminals
if (method === "choco" && err.data.stderr.includes("not running from an elevated command shell")) {
prompts.log.error("Please run the terminal as Administrator and try again")
} else {
prompts.log.error(err.data.stderr)
}
} else if (err instanceof Error) prompts.log.error(err.message)
prompts.outro("Done")
return
Expand Down
6 changes: 4 additions & 2 deletions packages/voltcode/src/cli/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { Installation } from "@/installation"

export async function upgrade() {
const config = await Config.global()
const latest = await Installation.latest().catch(() => {})
const method = await Installation.method()
const latest = await Installation.latest(method).catch(() => {})
if (!latest) return
if (Installation.VERSION === latest) return

Expand All @@ -17,7 +18,8 @@ export async function upgrade() {
return
}

await Installation.upgrade("curl", latest)
if (method === "unknown") return
await Installation.upgrade(method, latest)
.then(() => Bus.publish(Installation.Event.Updated, { version: latest }))
.catch(() => {})
}
179 changes: 161 additions & 18 deletions packages/voltcode/src/installation/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { BusEvent } from "@/bus/bus-event"
import path from "path"
import { $ } from "bun"
import z from "zod"
import { NamedError } from "@opencode-ai/util/error"
import { Log } from "../util/log"
import { iife } from "@/util/iife"
import { Flag } from "../flag/flag"

declare global {
Expand Down Expand Up @@ -56,7 +58,59 @@ export namespace Installation {
}

export async function method() {
return "curl" as const
if (process.execPath.includes(path.join(".voltcode", "bin"))) return "curl"
if (process.execPath.includes(path.join(".local", "bin"))) return "curl"
const exec = process.execPath.toLowerCase()

const checks = [
{
name: "npm" as const,
command: () => $`npm list -g --depth=0`.throws(false).quiet().text(),
},
{
name: "yarn" as const,
command: () => $`yarn global list`.throws(false).quiet().text(),
},
{
name: "pnpm" as const,
command: () => $`pnpm list -g --depth=0`.throws(false).quiet().text(),
},
{
name: "bun" as const,
command: () => $`bun pm ls -g`.throws(false).quiet().text(),
},
{
name: "brew" as const,
command: () => $`brew list --formula voltcode`.throws(false).quiet().text(),
},
{
name: "scoop" as const,
command: () => $`scoop list voltcode`.throws(false).quiet().text(),
},
{
name: "choco" as const,
command: () => $`choco list --limit-output voltcode`.throws(false).quiet().text(),
},
]

checks.sort((a, b) => {
const aMatches = exec.includes(a.name)
const bMatches = exec.includes(b.name)
if (aMatches && !bMatches) return -1
if (!aMatches && bMatches) return 1
return 0
})

for (const check of checks) {
const output = await check.command()
const installedName =
check.name === "brew" || check.name === "choco" || check.name === "scoop" ? "voltcode" : "voltcode-ai"
if (output.includes(installedName)) {
return check.name
}
}

return "unknown"
}

export const UpgradeFailedError = NamedError.create(
Expand All @@ -66,17 +120,58 @@ export namespace Installation {
}),
)

export async function upgrade(_method: Method, target: string) {
const result = await $`curl -fsSL https://www.voltropy.com/install | sh`.env({
...process.env,
VOLT_VERSION: target,
}).quiet().throws(false)
async function getBrewFormula() {
const tapFormula = await $`brew list --formula anomalyco/tap/voltcode`.throws(false).quiet().text()
if (tapFormula.includes("voltcode")) return "anomalyco/tap/voltcode"
const coreFormula = await $`brew list --formula voltcode`.throws(false).quiet().text()
if (coreFormula.includes("voltcode")) return "voltcode"
return "voltcode"
}

export async function upgrade(method: Method, target: string) {
let cmd
switch (method) {
case "curl":
cmd = $`curl -fsSL https://opencode.ai/install | bash`.env({
...process.env,
VERSION: target,
})
break
case "npm":
cmd = $`npm install -g voltcode-ai@${target}`
break
case "pnpm":
cmd = $`pnpm install -g voltcode-ai@${target}`
break
case "bun":
cmd = $`bun install -g voltcode-ai@${target}`
break
case "brew": {
const formula = await getBrewFormula()
cmd = $`brew upgrade ${formula}`.env({
HOMEBREW_NO_AUTO_UPDATE: "1",
...process.env,
})
break
}
case "choco":
cmd = $`echo Y | choco upgrade voltcode --version=${target}`
break
case "scoop":
cmd = $`scoop install voltcode@${target}`
break
default:
throw new Error(`Unknown method: ${method}`)
}
const result = await cmd.quiet().throws(false)
if (result.exitCode !== 0) {
const stderr = method === "choco" ? "not running from an elevated command shell" : result.stderr.toString("utf8")
throw new UpgradeFailedError({
stderr: result.stderr.toString("utf8"),
stderr: stderr,
})
}
log.info("upgraded", {
method,
target,
stdout: result.stdout.toString(),
stderr: result.stderr.toString(),
Expand All @@ -88,16 +183,64 @@ export namespace Installation {
export const CHANNEL = typeof VOLTCODE_CHANNEL === "string" ? VOLTCODE_CHANNEL : "local"
export const USER_AGENT = `voltcode/${CHANNEL}/${VERSION}/${Flag.VOLTCODE_CLIENT}`

export async function latest(_installMethod?: Method) {
const platform = process.platform === "darwin" ? "darwin" : "linux"
const arch = process.arch === "arm64" ? "arm64" : "amd64"
const res = await fetch("https://api.voltropy.com/v1/bootstrap/download-url", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ platform, arch, version: "latest" }),
})
if (!res.ok) throw new Error(res.statusText)
const data: any = await res.json()
return (data.version as string).replace(/^v/, "")
export async function latest(installMethod?: Method) {
const detectedMethod = installMethod || (await method())

if (detectedMethod === "brew") {
const formula = await getBrewFormula()
if (formula === "voltcode") {
return fetch("https://formulae.brew.sh/api/formula/opencode.json")
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data: any) => data.versions.stable)
}
}

if (detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm") {
const registry = await iife(async () => {
const r = (await $`npm config get registry`.quiet().nothrow().text()).trim()
const reg = r || "https://registry.npmjs.org"
return reg.endsWith("/") ? reg.slice(0, -1) : reg
})
const channel = CHANNEL
return fetch(`${registry}/voltcode-ai/${channel}`)
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data: any) => data.version)
}

if (detectedMethod === "choco") {
return fetch(
"https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27opencode%27%20and%20IsLatestVersion&$select=Version",
{ headers: { Accept: "application/json;odata=verbose" } },
)
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data: any) => data.d.results[0].Version)
}

if (detectedMethod === "scoop") {
return fetch("https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/opencode.json", {
headers: { Accept: "application/json" },
})
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data: any) => data.version)
}

return fetch("https://api.github.com/repos/anomalyco/opencode/releases/latest")
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data: any) => data.tag_name.replace(/^v/, ""))
}
}
2 changes: 1 addition & 1 deletion packages/voltcode/src/session/large-tool-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export async function handleLargeToolOutput(input: {
preview,
hasMore ? `\n...[${tokenCount - Token.estimate(preview)} more tokens]` : "",
``,
`To access the full output, use the Read tool with the original file path, or use lcm_describe with file_id "${fileId}" to see metadata about this stored output.`,
`The full output is stored in LCM. To retrieve it, spawn an explore sub-agent: Task(subagent_type="explore", prompt="Use lcm_read on ${fileId} to find <what you need>"). Use lcm_describe for metadata only. Do NOT attempt to read this content with the Read tool — it is stored in the LCM database, not as a file on disk.`,
].join("\n")

return {
Expand Down
Loading
Loading