refactor(cli): define server connection boundary (#37133)

This commit is contained in:
Kit Langton 2026-07-15 11:48:38 -04:00 committed by GitHub
commit f5dd181443
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 250 additions and 121 deletions

View file

@ -30,6 +30,50 @@ This proposal does not introduce a supervisor process, warm candidate server,
protocol negotiation, idle background restart, or general execution-recovery
framework.
## Architecture at a Glance
```text
╭───────────────────╮
│ CLI ServiceConfig │
╰─────────┬─────────╯
╭──────────────────────╮
│ CLI ServerConnection │
╰───────────┬──────────╯
╭──────────────────╰───────────────────╮
▼ ▼
╭──────────────────────────╮ ╭─────────────────────────╮
│ Client Service lifecycle │ │ CLI runPromiseWith seam │
╰─────────────┬────────────╯ ╰─────────────┬───────────╯
╰─────╮ │
▼ ▼
╭────────────────────────────╮ ╭─────────────╮
│ Background service process │ │ TUI / Solid │
╰──────────────┬─────────────╯ ╰──────┬──────╯
│ │
╰────────────◀────────────────────╯
╭───────────────────────╮
│ Server HTTP transport │
╰───────────┬───────────╯
╭──────────────────╮
│ Core application │
╰──────────────────╯
```
| Owner | Responsibility |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| `packages/client/src/effect/service.ts` | Effect-native discovery, start, and stop lifecycle operations |
| `packages/cli/src/services/service-config.ts` | CLI registration path, installed version, and daemon command |
| `packages/cli/src/services/server-connection.ts` | Resolve an endpoint and, only for the shared service, grouped reconnect and restart Effects |
| `packages/cli/src/server-process.ts` | Daemon election, registration, and server process boot |
| `packages/server/src/process.ts` | HTTP lifecycle shell and application transport |
| `packages/core` | Application behavior behind the transport |
| CLI default handler | Convert lifecycle Effects with the outer `FileSystem` context and pass grouped Promise capabilities |
| `packages/tui` Solid client context | Own event-stream reconnect, endpoint replacement, status, and user-triggered restart UI |
## Implementation Status
| Area | State |
@ -164,19 +208,22 @@ This design gives each concept one authority.
## System Model
```mermaid
flowchart LR
TUI[Fresh or existing TUI]
REG[Registration file]
LOCK[Process-held OS service lock]
SHELL[Lifecycle shell]
APP[OpenCode application]
TUI -->|discover| REG
TUI -->|observe| SHELL
TUI -->|normal requests| APP
SHELL --> APP
LOCK -->|authorizes one owner| SHELL
```text
╭───────────────────────╮ ╭──────────────────────────────╮
│ Fresh or existing TUI │ │ Process-held OS service lock │
╰───────────┬───────────╯ ╰───────────────┬──────────────╯
╰─────┬ normal requests observe ───────────────────────╮ │
│ discover │ ├──╯ authorizes one owner
▼ │ ▼
╭───────────────────╮ │ ╭─────────────────╮
│ Registration file │ │ │ Lifecycle shell │
╰───────────────────╯ │ ╰────────┬────────╯
│ │
├────────────────────────╯
╭──────────────────────╮
│ OpenCode application │
╰──────────────────────╯
```
The lifecycle shell and application run in the same process. The distinction is
@ -311,24 +358,39 @@ Windows, where Bun FFI is not available on every shipped architecture. It lives
alongside the existing utility in `packages/core/src/util`. This primitive is
the foundation of the design, so the delivery sequence spikes it first.
```mermaid
flowchart TD
A[Contender process starts]
B{Acquire service lock?}
C[Exit successfully]
D[Bind lifecycle shell]
E[Write registration]
F[Report starting]
G[Initialize application]
H[Report ready]
I[Serve until shutdown]
A --> B
B -->|No| C
B -->|Yes| D
D --> E --> F --> G
G -->|Success| H --> I
G -->|Failure| J[Report failed and stay bound]
```text
Contender Lock Lifecycle Application
│ │ │ │
├─ try acquire ───▶ │ │
│ │ │ │
╭─ alt: lock held ────────────────────────────────────────────────╮
│ │ │ │ │ │
│ ◀─ busy ──────────┤ │ │ │
│ │ │ │ │ │
│ ├─────────╮ │ │ │ │
│ │ exit │ │ │ │ │
│ ◀─────────╯ │ │ │ │
│ │ │ │ │ │
├─ else: lock acquired ───────────────────────────────────────────┤
│ │ │ │ │ │
│ ◀─ owner ─────────┤ │ │ │
│ │ │ │ │ │
│ ├─ bind, register, starting ────────▶ │ │
│ │ │ │ │ │
│ ├─ initialize ──────────────────────────────────────────────▶ │
│ │ │ │ │ │
│╭─ alt: boot succeeds ──────────────────────────────────────────╮│
││ │ │ │ │ ││
││ │ │ ◀─ ready ───────────────┤ ││
││ │ │ │ │ ││
│├─ else: boot fails ────────────────────────────────────────────┤│
││ │ │ │ │ ││
││ │ │ ◀─ failed, stay bound ──┤ ││
││ │ │ │ │ ││
│╰───────────────────────────────────────────────────────────────╯│
│ │ │ │ │ │
╰─────────────────────────────────────────────────────────────────╯
│ │ │ │
```
Lock acquisition by a contender is nonblocking or tightly bounded. A loser

View file

@ -3,7 +3,7 @@ import { Effect, Option } from "effect"
import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime"
import { Service } from "@opencode-ai/client/effect"
import { Server } from "../../services/server"
import { ServerConnection } from "../../services/server-connection"
const methods = new Set(["delete", "get", "head", "options", "patch", "post", "put"])
@ -18,7 +18,7 @@ type OpenApi = {
export default Runtime.handler(
Commands.commands.api,
Effect.fn("cli.api")(function* (input) {
const server = yield* Server.resolve({
const server = yield* ServerConnection.resolve({
server: Option.getOrUndefined(input.server),
standalone: input.standalone,
mismatch: "ignore",
@ -62,11 +62,7 @@ export function rawRequest(input: readonly string[]) {
return { method: input[0].toUpperCase(), path: input[1] }
}
function resolveRequest(
endpoint: Service.Endpoint,
input: readonly string[],
params: Record<string, string>,
) {
function resolveRequest(endpoint: Service.Endpoint, input: readonly string[], params: Record<string, string>) {
const raw = rawRequest(input)
if (raw) return Effect.succeed(raw)
if (input.length !== 1) return Effect.fail(new Error("Expected an operation name or an HTTP method and path"))

View file

@ -4,8 +4,8 @@ import { run } from "@opencode-ai/tui"
import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime"
import { Config } from "../../config"
import { Effect, Option } from "effect"
import { Server } from "../../services/server"
import { Context, Effect, FileSystem, Option } from "effect"
import { ServerConnection } from "../../services/server-connection"
import { Updater } from "../../services/updater"
import { UpdatePreflight } from "../../services/update-preflight"
import { Npm } from "@opencode-ai/core/npm"
@ -18,7 +18,7 @@ export default Runtime.handler(Commands, (input) =>
yield* updater.check().pipe(Effect.forkScoped)
const preflight = UpdatePreflight.make()
yield* Effect.addFinalizer(() => Effect.promise(() => preflight.close()))
const server = yield* Server.resolve({
const server = yield* ServerConnection.resolve({
server: Option.getOrUndefined(input.server),
standalone: input.standalone,
onStart: (reason, existing) => {
@ -37,11 +37,22 @@ export default Runtime.handler(Commands, (input) =>
preflight.loading()
const config = yield* Config.Service
const npm = yield* Npm.Service
const context = yield* Effect.context()
const fileSystem = yield* FileSystem.FileSystem
const runServicePromise = Effect.runPromiseWith(Context.make(FileSystem.FileSystem, fileSystem))
const context = yield* Effect.context<FileSystem.FileSystem>()
const runFork = Effect.runForkWith(context)
const runPromise = Effect.runPromiseWith(context)
const service = server.service
yield* run({
server,
server: {
endpoint: server.endpoint,
service: service
? {
reconnect: (onStatus, signal) => runServicePromise(service.reconnect(onStatus), { signal }),
restart: () => runServicePromise(service.restart()),
}
: undefined,
},
args: { continue: input.continue, sessionID: Option.getOrUndefined(input.session) },
config: {
path: config.path,

View file

@ -1,14 +1,14 @@
import { Effect, Option } from "effect"
import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime"
import { Server } from "../../services/server"
import { ServerConnection } from "../../services/server-connection"
export default Runtime.handler(Commands.commands.mini, (input) =>
Effect.gen(function* () {
const { runMini, validateMiniTerminal } = yield* Effect.promise(() => import("../../mini"))
yield* Effect.promise(async () => validateMiniTerminal())
const serverURL = Option.getOrUndefined(input.server)
const server = yield* Server.resolve({ server: serverURL, standalone: input.standalone })
const server = yield* ServerConnection.resolve({ server: serverURL, standalone: input.standalone })
yield* Effect.promise(() =>
runMini({
server,

View file

@ -1,13 +1,13 @@
import { Effect, Option } from "effect"
import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime"
import { Server } from "../../services/server"
import { ServerConnection } from "../../services/server-connection"
export default Runtime.handler(Commands.commands.run, (input) =>
Effect.gen(function* () {
const { runNonInteractive } = yield* Effect.promise(() => import("../../mini"))
const separator = process.argv.indexOf("--", 2)
const server = yield* Server.resolve({
const server = yield* ServerConnection.resolve({
server: Option.getOrUndefined(input.server),
standalone: input.standalone,
})

View file

@ -1,12 +1,12 @@
import { Service } from "@opencode-ai/client/effect"
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
import { Server } from "../services/server"
import { ServerConnection } from "../services/server-connection"
import { waitForCatalogReady } from "./catalog.shared"
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./runtime.stdin"
import type { RunInput, RunTuiConfig } from "./types"
export type MiniCommandInput = {
server: Server.Resolved
server: ServerConnection.Resolved
continue?: boolean
session?: string
fork?: boolean
@ -38,10 +38,7 @@ export async function runMini(input: MiniCommandInput) {
return agentTask
}
const resolveSession = async () => {
const [agent, selected] = await Promise.all([
resolveAgent(),
selectSession(sdk, directory, input),
])
const [agent, selected] = await Promise.all([resolveAgent(), selectSession(sdk, directory, input)])
const readyModel =
model ?? (selected?.model ? { providerID: selected.model.providerID, modelID: selected.model.id } : undefined)
if (readyModel) await waitForCatalogReady({ sdk, directory, model: readyModel })

View file

@ -4,7 +4,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { Model } from "@opencode-ai/schema/model"
import { open } from "node:fs/promises"
import path from "node:path"
import { Server } from "../services/server"
import { ServerConnection } from "../services/server-connection"
import { loadRunAgents, waitForCatalogReady } from "./catalog.shared"
import { runNonInteractivePrompt } from "./noninteractive"
import { toolInlineInfo } from "./tool"
@ -12,7 +12,7 @@ import type { MiniToolPart } from "./types"
import { UI } from "./ui"
export type RunCommandInput = {
server: Server.Resolved
server: ServerConnection.Resolved
message: string[]
continue?: boolean
session?: string
@ -73,8 +73,7 @@ async function execute(input: RunCommandInput, prepared: Prepared, endpoint: Ser
.then((result) => (result.data ? { providerID: result.data.providerID, modelID: result.data.id } : undefined))
: undefined
const model = pickRunModel(explicitModel, variant, sessionModel, defaultModel)
if (variant && !model)
return reportError(input, "Cannot select a variant before selecting a model", session?.id)
if (variant && !model) return reportError(input, "Cannot select a variant before selecting a model", session?.id)
if (model) {
await waitForCatalogReady({ sdk: client, directory: cwd, workspace, model })
const available = await client.model.list({ location: { directory: cwd, workspace } })

View file

@ -1,4 +1,3 @@
import { NodeFileSystem } from "@effect/platform-node"
import { Service } from "@opencode-ai/client/effect"
import { ClientError, isUnauthorizedError, OpenCode } from "@opencode-ai/client/promise"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
@ -16,11 +15,10 @@ export type Args = {
export type Resolved = {
readonly endpoint: Service.Endpoint
readonly reconnect?: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise<Service.Endpoint>
readonly reload?: () => Promise<void>
readonly service?: ReturnType<typeof managedService>
}
export const resolve = Effect.fn("cli.server.resolve")(function* (args: Args) {
export const resolve = Effect.fn("cli.server-connection.resolve")(function* (args: Args) {
if (args.server !== undefined && args.standalone)
return yield* Effect.fail(new Error("--server and --standalone cannot be combined"))
if (args.server !== undefined) {
@ -45,24 +43,24 @@ export const resolve = Effect.fn("cli.server.resolve")(function* (args: Args) {
}
const options = yield* ServiceConfig.options()
const endpoint = yield* resolveManaged({ ...options, onStart: args.onStart }, args.mismatch ?? "replace")
const reconnectOptions = { ...options, version: undefined }
return {
endpoint,
reconnect: (onStatus, signal) =>
Effect.runPromise(Service.start({ ...reconnectOptions, onStatus }).pipe(Effect.provide(NodeFileSystem.layer)), {
signal,
}),
reload: () =>
Effect.runPromise(
Effect.gen(function* () {
yield* Service.stop(options, { targetVersion: options.version })
yield* Service.start(options)
}).pipe(Effect.provide(NodeFileSystem.layer)),
),
endpoint: yield* resolveManaged({ ...options, onStart: args.onStart }, args.mismatch ?? "replace"),
service: managedService(options),
} satisfies Resolved
})
function managedService(options: Service.StartOptions) {
const reconnectOptions = { ...options, version: undefined }
return {
reconnect: (onStatus: (status: Service.Status) => void) => Service.start({ ...reconnectOptions, onStatus }),
restart: () =>
Effect.gen(function* () {
yield* Service.stop(options, { targetVersion: options.version })
yield* Service.start(options)
}),
}
}
const resolveManaged = Effect.fnUntraced(function* (
options: Service.StartOptions,
mismatch: NonNullable<Args["mismatch"]>,
@ -92,4 +90,4 @@ function connectError(endpoint: Service.Endpoint, cause: unknown) {
return new Error(`Server at ${endpoint.url} did not provide a compatible V2 health response`, { cause })
}
export * as Server from "./server"
export * as ServerConnection from "./server-connection"

View file

@ -0,0 +1,59 @@
import { NodeFileSystem } from "@effect/platform-node"
import { Global } from "@opencode-ai/core/global"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { expect, test } from "bun:test"
import { Effect, FileSystem, Scope } from "effect"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { ServerConnection } from "../src/services/server-connection"
import { ServiceConfig } from "../src/services/service-config"
test("resolution groups Effect-native lifecycle operations only for the managed service", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-server-resolution-"))
const id = "server-resolution-test"
const server = Bun.serve({
port: 0,
fetch() {
return Response.json({
healthy: true,
version: InstallationVersion,
pid: process.pid,
instanceID: id,
status: { type: "ready" },
})
},
})
const registration = path.join(root, "state", ServiceConfig.filename())
const layer = Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") })
const runPromise = <A, E>(effect: Effect.Effect<A, E, Global.Service | FileSystem.FileSystem | Scope.Scope>) =>
Effect.runPromise(effect.pipe(Effect.provide(layer), Effect.provide(NodeFileSystem.layer), Effect.scoped))
try {
await fs.mkdir(path.dirname(registration), { recursive: true })
await fs.writeFile(
registration,
JSON.stringify({
id,
version: InstallationVersion,
url: server.url.toString(),
pid: process.pid,
}),
)
const resolved = await runPromise(ServerConnection.resolve({}))
expect(resolved.endpoint.url).toBe(server.url.toString())
expect(resolved.service).toBeDefined()
if (!resolved.service) throw new Error("Expected managed service capabilities")
expect(Effect.isEffect(resolved.service.reconnect(() => {}))).toBe(true)
expect(Effect.isEffect(resolved.service.restart())).toBe(true)
expect(await runPromise(resolved.service.reconnect(() => {}))).toEqual(resolved.endpoint)
const explicit = await runPromise(ServerConnection.resolve({ server: server.url.toString() }))
expect(explicit.endpoint.url).toBe(server.url.toString())
expect(explicit.service).toBeUndefined()
} finally {
await server.stop(true)
await fs.rm(root, { recursive: true, force: true })
}
})

View file

@ -4,8 +4,8 @@ description: "Diagnose OpenCode startup, server, and session issues."
---
<Tip>
You can ask OpenCode to debug itself. Describe the problem and ask it to use this troubleshooting page; it can read the
steps below, inspect its service and logs, and help identify the issue.
You can ask OpenCode to debug itself. Describe the problem and ask it to use this troubleshooting page; it can read
the steps below, inspect its service and logs, and help identify the issue.
</Tip>
OpenCode runs as two processes: the TUI is a client, while a background server owns sessions, plugins, permissions, and
@ -31,10 +31,10 @@ If the service is stuck or unhealthy, restart it:
opencode2 service restart
```
From inside the TUI, run `/reload` to restart the managed service and reconnect:
From inside the TUI, run `/restart` to restart the managed service and reconnect:
```text
/reload
/restart
```
You can also stop and start it explicitly:
@ -45,8 +45,8 @@ opencode2 service start
```
<Note>
OpenCode normally discovers or starts the shared background service automatically. The service commands are only needed
when diagnosing its lifecycle.
OpenCode normally discovers or starts the shared background service automatically. The service commands are only
needed when diagnosing its lifecycle.
</Note>
## Run an isolated session
@ -125,8 +125,8 @@ The database normally lives at:
`OPENCODE_DB` can override the database location.
<Warning>
Do not delete or edit service files or the database while troubleshooting. Use the service commands to manage the daemon,
and make a backup before inspecting persistent data with external tools.
Do not delete or edit service files or the database while troubleshooting. Use the service commands to manage the
daemon, and make a backup before inspecting persistent data with external tools.
</Warning>
## Explicit servers

View file

@ -118,6 +118,7 @@ const appBindingCommands = [
"provider.connect",
"opencode.status",
"server.pair",
"service.restart",
"opencode.debug",
"theme.switch",
"theme.switch_mode",
@ -138,8 +139,10 @@ const appBindingCommands = [
export type TuiInput = {
server: {
endpoint: Service.Endpoint
reconnect?: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise<Service.Endpoint>
reload?: () => Promise<void>
service?: {
reconnect: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise<Service.Endpoint>
restart: () => Promise<void>
}
}
args: Args
config: Config.Interface
@ -183,14 +186,15 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
Effect.catch(() => Effect.tryPromise(() => api.location.get()).pipe(Effect.map((response) => response.directory))),
)
const handoff = input.terminalHandoff ? yield* Effect.promise(input.terminalHandoff) : undefined
const reconnectEndpoint = input.server.reconnect
const reconnect = reconnectEndpoint
? async (onStatus: (status: Service.Status) => void, signal: AbortSignal) => {
const endpoint = await reconnectEndpoint(onStatus, signal)
const next = { baseUrl: endpoint.url, headers: Service.headers(endpoint) }
return {
api: OpenCode.make(next),
}
const managed = input.server.service
const service = managed
? {
reconnect: async (onStatus: (status: Service.Status) => void, signal: AbortSignal) => {
const endpoint = await managed.reconnect(onStatus, signal)
const next = { baseUrl: endpoint.url, headers: Service.headers(endpoint) }
return { api: OpenCode.make(next) }
},
restart: managed.restart,
}
: undefined
const exit = { epilogue: undefined as string | undefined, reason: undefined as unknown }
@ -324,7 +328,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
}
>
<PluginRuntimeProvider value={pluginRuntime}>
<ClientProvider api={api} reconnect={reconnect} reload={input.server.reload}>
<ClientProvider api={api} service={service}>
<PermissionProvider>
<DataProvider>
<LocationProvider>
@ -757,19 +761,21 @@ function App(props: { pair?: DialogPairCredentials }) {
},
category: "System",
},
...(client.reload
...(client.restart
? [
{
name: "server.reload",
title: "Reload server",
slash: { name: "reload" },
name: "service.restart",
title: "Restart service",
slash: { name: "restart" },
run: async () => {
const restart = client.restart
if (!restart) return
dialog.clear()
toast.show({ variant: "info", message: "Reloading server...", duration: 30000 })
// reload resolves once the replacement service is healthy; the
toast.show({ variant: "info", message: "Restarting service...", duration: 30000 })
// restart resolves once the replacement service is healthy; the
// event stream reattaches through the reconnect loop.
await client.reload!()
.then(() => toast.show({ variant: "success", message: "Server reloaded" }))
await restart()
.then(() => toast.show({ variant: "success", message: "Service restarted" }))
.catch(toast.error)
},
category: "System",

View file

@ -18,18 +18,18 @@ export type ClientConnectionEvent = {
}
}
type ManagedService = {
reconnect: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise<{ api: OpenCodeClient }>
restart: () => Promise<void>
}
type ClientEventMap = { [Type in OpenCodeEvent["type"]]: Extract<OpenCodeEvent, { type: Type }> }
const connectTimeout = 2_000
const connectionHistoryLimit = 50
export const { use: useClient, provider: ClientProvider } = createSimpleContext({
name: "Client",
init: (props: {
api: OpenCodeClient
reconnect?: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise<{ api: OpenCodeClient }>
// Stops and starts the managed service; present only in service mode.
reload?: () => Promise<void>
}) => {
init: (props: { api: OpenCodeClient; service?: ManagedService }) => {
const log = useLog({ component: "client" })
const abort = new AbortController()
const history: ClientConnectionEvent[] = []
@ -115,8 +115,8 @@ export const { use: useClient, provider: ClientProvider } = createSimpleContext(
// Re-resolve the transport before retrying: the server may have
// moved (service restarted on a new port) or need starting. Static
// transports (--server, standalone) resolve to the same address.
if (props.reconnect) {
const next = await props.reconnect(setService, controller.signal).catch((error) => {
if (props.service) {
const next = await props.service.reconnect(setService, controller.signal).catch((error) => {
if (!controller.signal.aborted)
log.info("server resolution failed", {
attempt,
@ -168,7 +168,7 @@ export const { use: useClient, provider: ClientProvider } = createSimpleContext(
},
},
},
reload: props.reload,
restart: props.service?.restart,
}
},
})

View file

@ -65,10 +65,11 @@ async function mount(
const ready = new Promise<void>((resolve) => {
done = resolve
})
const service = reconnect ? { reconnect, restart: () => Promise.resolve() } : undefined
const app = await testRender(() => (
<TestTuiContexts log={log}>
<ClientProvider api={createApi(calls.fetch)} reconnect={reconnect}>
<ClientProvider api={createApi(calls.fetch)} service={service}>
<Probe
onReady={(ctx) => {
client = ctx.client

View file

@ -4,8 +4,8 @@ description: "Diagnose OpenCode startup, server, and session issues."
---
<Tip>
You can ask OpenCode to debug itself. Describe the problem and ask it to use this troubleshooting page; it can read the
steps below, inspect its service and logs, and help identify the issue.
You can ask OpenCode to debug itself. Describe the problem and ask it to use this troubleshooting page; it can read
the steps below, inspect its service and logs, and help identify the issue.
</Tip>
OpenCode runs as two processes: the TUI is a client, while a background server owns sessions, plugins, permissions, and
@ -31,10 +31,10 @@ If the service is stuck or unhealthy, restart it:
opencode2 service restart
```
From inside the TUI, run `/reload` to restart the managed service and reconnect:
From inside the TUI, run `/restart` to restart the managed service and reconnect:
```text
/reload
/restart
```
You can also stop and start it explicitly:
@ -45,8 +45,8 @@ opencode2 service start
```
<Note>
OpenCode normally discovers or starts the shared background service automatically. The service commands are only needed
when diagnosing its lifecycle.
OpenCode normally discovers or starts the shared background service automatically. The service commands are only
needed when diagnosing its lifecycle.
</Note>
## Run an isolated session
@ -125,8 +125,8 @@ The database normally lives at:
`OPENCODE_DB` can override the database location.
<Warning>
Do not delete or edit service files or the database while troubleshooting. Use the service commands to manage the daemon,
and make a backup before inspecting persistent data with external tools.
Do not delete or edit service files or the database while troubleshooting. Use the service commands to manage the
daemon, and make a backup before inspecting persistent data with external tools.
</Warning>
## Explicit servers