mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-31 15:34:02 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1927969e8 | |||
| 45f257da73 | |||
| 8123247d7a | |||
| ce69ddb88e |
@@ -1,6 +1,5 @@
|
||||
import { Context } from "./context"
|
||||
import { UserRole } from "./schema/user.sql"
|
||||
import { Log } from "./util/log"
|
||||
|
||||
export namespace Actor {
|
||||
interface Account {
|
||||
@@ -38,8 +37,6 @@ export namespace Actor {
|
||||
const ctx = Context.create<Info>()
|
||||
export const use = ctx.use
|
||||
|
||||
const log = Log.create().tag("namespace", "actor")
|
||||
|
||||
export function provide<R, T extends Info["type"]>(
|
||||
type: T,
|
||||
properties: Extract<Info, { type: T }>["properties"],
|
||||
@@ -50,12 +47,7 @@ export namespace Actor {
|
||||
type,
|
||||
properties,
|
||||
} as any,
|
||||
() => {
|
||||
return Log.provide({ ...properties }, () => {
|
||||
log.info("provided")
|
||||
return cb()
|
||||
})
|
||||
},
|
||||
cb,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Context } from "../context"
|
||||
|
||||
export namespace Log {
|
||||
const ctx = Context.create<{
|
||||
tags: Record<string, any>
|
||||
}>()
|
||||
|
||||
export function create(tags?: Record<string, any>) {
|
||||
tags = tags || {}
|
||||
|
||||
const result = {
|
||||
info(message?: any, extra?: Record<string, any>) {
|
||||
const prefix = Object.entries({
|
||||
...use().tags,
|
||||
...tags,
|
||||
...extra,
|
||||
})
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join(" ")
|
||||
console.log(prefix, message)
|
||||
return result
|
||||
},
|
||||
tag(key: string, value: string) {
|
||||
if (tags) tags[key] = value
|
||||
return result
|
||||
},
|
||||
clone() {
|
||||
return Log.create({ ...tags })
|
||||
},
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function provide<R>(tags: Record<string, any>, cb: () => R) {
|
||||
const existing = use()
|
||||
return ctx.provide(
|
||||
{
|
||||
tags: {
|
||||
...existing.tags,
|
||||
...tags,
|
||||
},
|
||||
},
|
||||
cb,
|
||||
)
|
||||
}
|
||||
|
||||
function use() {
|
||||
try {
|
||||
return ctx.use()
|
||||
} catch {
|
||||
return { tags: {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,79 @@
|
||||
import { Cause, Effect, Logger, References } from "effect"
|
||||
import * as Log from "../util/log"
|
||||
import { appendFileSync } from "fs"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Cause, Effect, Layer, Logger, References, Schema } from "effect"
|
||||
import * as Global from "../global"
|
||||
import { ensureProcessMetadata } from "../util/opencode-process"
|
||||
|
||||
type Fields = Record<string, unknown>
|
||||
|
||||
const normalizeKey = (key: string) => (key === "sessionID" ? "session.id" : key)
|
||||
export const Level = Schema.Literals(["DEBUG", "INFO", "WARN", "ERROR"]).annotate({
|
||||
identifier: "LogLevel",
|
||||
description: "Log level",
|
||||
})
|
||||
export type Level = Schema.Schema.Type<typeof Level>
|
||||
|
||||
export interface Handle {
|
||||
readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
|
||||
readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
|
||||
readonly warn: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
|
||||
readonly error: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
|
||||
readonly with: (extra: Fields) => Handle
|
||||
const levelPriority: Record<Level, number> = {
|
||||
DEBUG: 0,
|
||||
INFO: 1,
|
||||
WARN: 2,
|
||||
ERROR: 3,
|
||||
}
|
||||
|
||||
const clean = (input?: Fields): Fields =>
|
||||
const normalizeKey = (key: string) => (key === "sessionID" ? "session.id" : key)
|
||||
|
||||
const clean = (input?: object): Fields =>
|
||||
Object.fromEntries(
|
||||
Object.entries(input ?? {})
|
||||
.filter((entry) => entry[1] !== undefined && entry[1] !== null)
|
||||
.map(([key, value]) => [normalizeKey(key), value]),
|
||||
)
|
||||
|
||||
const text = (input: unknown): string => {
|
||||
// oxlint-disable-next-line no-base-to-string
|
||||
if (Array.isArray(input)) return input.map((item) => String(item)).join(" ")
|
||||
// oxlint-disable-next-line no-base-to-string
|
||||
return input === undefined ? "" : String(input)
|
||||
export function file() {
|
||||
if (disabled(process.env.OPENCODE_LOG_FILE)) return ""
|
||||
return process.env.OPENCODE_LOG_FILE || path.join(Global.Path.log, "log.jsonl")
|
||||
}
|
||||
|
||||
const call = (run: (msg?: unknown) => Effect.Effect<void>, base: Fields, msg?: unknown, extra?: Fields) => {
|
||||
const ann = clean({ ...base, ...extra })
|
||||
const fx = run(msg)
|
||||
return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx
|
||||
function shouldLog(input: Level): boolean {
|
||||
return levelPriority[input] >= levelPriority[parseLevel(process.env.OPENCODE_LOG_LEVEL) ?? "INFO"]
|
||||
}
|
||||
|
||||
export const logger = Logger.make((opts) => {
|
||||
function write(input: { json: string; pretty: string }) {
|
||||
const target = file()
|
||||
if (target) {
|
||||
try {
|
||||
appendFileSync(target, input.json)
|
||||
} catch {}
|
||||
}
|
||||
if (truthy(process.env.OPENCODE_PRINT_LOGS)) process.stderr.write(input.pretty)
|
||||
}
|
||||
|
||||
function build(inputLevel: Level, ts: Date, message: unknown, fields: Fields): { json: string; pretty: string } {
|
||||
const metadata = ensureProcessMetadata("main")
|
||||
const service = typeof fields.service === "string" ? fields.service : undefined
|
||||
if (service) delete fields.service
|
||||
const text = stringifyMessage(message)
|
||||
const record = {
|
||||
ts: ts.toISOString(),
|
||||
level: inputLevel,
|
||||
message: text,
|
||||
run_id: metadata.runID,
|
||||
process_role: metadata.processRole,
|
||||
pid: process.pid,
|
||||
service,
|
||||
fields: Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, normalize(value)])),
|
||||
}
|
||||
const prefix = Object.entries({ service, ...record.fields })
|
||||
.filter((entry) => entry[1] !== undefined && entry[1] !== null)
|
||||
.map(([key, value]) => `${key}=${typeof value === "object" ? safeStringify(value) : value}`)
|
||||
.join(" ")
|
||||
return {
|
||||
json: safeStringify(record) + "\n",
|
||||
pretty: [inputLevel.padEnd(5), ts.toISOString().split(".")[0], prefix, text].filter(Boolean).join(" ") + "\n",
|
||||
}
|
||||
}
|
||||
|
||||
const logger = Logger.make((opts) => {
|
||||
const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations))
|
||||
const now = opts.date.getTime()
|
||||
for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) {
|
||||
@@ -43,31 +83,91 @@ export const logger = Logger.make((opts) => {
|
||||
extra.cause = Cause.pretty(opts.cause)
|
||||
}
|
||||
|
||||
const svc = typeof extra.service === "string" ? extra.service : undefined
|
||||
if (svc) delete extra.service
|
||||
const log = svc ? Log.create({ service: svc }) : Log.Default
|
||||
const msg = text(opts.message)
|
||||
|
||||
switch (opts.logLevel) {
|
||||
case "Trace":
|
||||
case "Debug":
|
||||
return log.debug(msg, extra)
|
||||
if (shouldLog("DEBUG")) write(build("DEBUG", opts.date, opts.message, extra))
|
||||
return
|
||||
case "Warn":
|
||||
return log.warn(msg, extra)
|
||||
if (shouldLog("WARN")) write(build("WARN", opts.date, opts.message, extra))
|
||||
return
|
||||
case "Error":
|
||||
case "Fatal":
|
||||
return log.error(msg, extra)
|
||||
if (shouldLog("ERROR")) write(build("ERROR", opts.date, opts.message, extra))
|
||||
return
|
||||
default:
|
||||
return log.info(msg, extra)
|
||||
if (shouldLog("INFO")) write(build("INFO", opts.date, opts.message, extra))
|
||||
}
|
||||
})
|
||||
|
||||
export const layer = Logger.layer([logger], { mergeWithExisting: false })
|
||||
type LoggerInput = Logger.Logger<unknown, unknown> | Effect.Effect<Logger.Logger<unknown, unknown>, unknown, unknown>
|
||||
|
||||
export const create = (base: Fields = {}): Handle => ({
|
||||
debug: (msg, extra) => call((item) => Effect.logDebug(item), base, msg, extra),
|
||||
info: (msg, extra) => call((item) => Effect.logInfo(item), base, msg, extra),
|
||||
warn: (msg, extra) => call((item) => Effect.logWarning(item), base, msg, extra),
|
||||
error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra),
|
||||
with: (extra) => create({ ...base, ...extra }),
|
||||
})
|
||||
const makeLayer = <const Loggers extends ReadonlyArray<LoggerInput>>(loggers: Loggers) =>
|
||||
Logger.layer(loggers, { mergeWithExisting: false }).pipe(
|
||||
Layer.tap(() =>
|
||||
Effect.promise(async () => {
|
||||
const target = file()
|
||||
if (target) await fs.mkdir(path.dirname(target), { recursive: true })
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
export const layer = makeLayer([logger])
|
||||
|
||||
export const layerWith = <const Loggers extends ReadonlyArray<LoggerInput>>(loggers: Loggers) =>
|
||||
makeLayer([logger, ...loggers] as const)
|
||||
|
||||
function truthy(value: string | undefined) {
|
||||
return value?.toLowerCase() === "1" || value?.toLowerCase() === "true"
|
||||
}
|
||||
|
||||
function disabled(value: string | undefined) {
|
||||
const lower = value?.toLowerCase()
|
||||
return lower === "0" || lower === "false" || lower === "off"
|
||||
}
|
||||
|
||||
function parseLevel(value: string | undefined): Level | undefined {
|
||||
if (value === "DEBUG" || value === "INFO" || value === "WARN" || value === "ERROR") return value
|
||||
return undefined
|
||||
}
|
||||
|
||||
function formatError(error: Error, depth = 0): string {
|
||||
const result = error.message
|
||||
return error.cause instanceof Error && depth < 10
|
||||
? result + " Caused by: " + formatError(error.cause, depth + 1)
|
||||
: result
|
||||
}
|
||||
|
||||
function stringifyMessage(message: unknown): string {
|
||||
if (message instanceof Error) return formatError(message)
|
||||
if (message === undefined) return ""
|
||||
if (typeof message === "string") return message
|
||||
if (Array.isArray(message)) return message.map((item) => stringifyMessage(item)).join(" ")
|
||||
if (typeof message === "object") return safeStringify(message)
|
||||
return String(message)
|
||||
}
|
||||
|
||||
function normalize(value: unknown): unknown {
|
||||
if (value instanceof Error) {
|
||||
return {
|
||||
name: value.name,
|
||||
message: formatError(value),
|
||||
stack: value.stack,
|
||||
}
|
||||
}
|
||||
if (typeof value === "bigint") return value.toString()
|
||||
return value
|
||||
}
|
||||
|
||||
function safeStringify(value: unknown) {
|
||||
const seen = new WeakSet<object>()
|
||||
return JSON.stringify(value, (_, item) => {
|
||||
if (typeof item === "bigint") return item.toString()
|
||||
if (item instanceof Error) return normalize(item)
|
||||
if (typeof item === "object" && item !== null) {
|
||||
if (seen.has(item)) return "[Circular]"
|
||||
seen.add(item)
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Effect, Layer, Logger } from "effect"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import { OtlpLogger, OtlpSerialization } from "effect/unstable/observability"
|
||||
import * as EffectLogger from "./logger"
|
||||
@@ -54,17 +54,13 @@ export function resource(): { serviceName: string; serviceVersion: string; attri
|
||||
}
|
||||
|
||||
function logs() {
|
||||
return Logger.layer(
|
||||
[
|
||||
EffectLogger.logger,
|
||||
OtlpLogger.make({
|
||||
url: `${base}/v1/logs`,
|
||||
resource: resource(),
|
||||
headers,
|
||||
}),
|
||||
],
|
||||
{ mergeWithExisting: false },
|
||||
).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer))
|
||||
return EffectLogger.layerWith([
|
||||
OtlpLogger.make({
|
||||
url: `${base}/v1/logs`,
|
||||
resource: resource(),
|
||||
headers,
|
||||
}),
|
||||
]).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer))
|
||||
}
|
||||
|
||||
const traces = async () => {
|
||||
|
||||
@@ -19,7 +19,7 @@ const paths = {
|
||||
},
|
||||
data,
|
||||
bin: path.join(cache, "bin"),
|
||||
log: path.join(data, "log"),
|
||||
log: state,
|
||||
repos: path.join(data, "repos"),
|
||||
cache,
|
||||
config,
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
export * as Log from "./log"
|
||||
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { createWriteStream } from "fs"
|
||||
import * as Global from "../global"
|
||||
import { Schema } from "effect"
|
||||
import { Glob } from "./glob"
|
||||
|
||||
export const Level = Schema.Literals(["DEBUG", "INFO", "WARN", "ERROR"]).annotate({
|
||||
identifier: "LogLevel",
|
||||
description: "Log level",
|
||||
})
|
||||
export type Level = Schema.Schema.Type<typeof Level>
|
||||
|
||||
const levelPriority: Record<Level, number> = {
|
||||
DEBUG: 0,
|
||||
INFO: 1,
|
||||
WARN: 2,
|
||||
ERROR: 3,
|
||||
}
|
||||
const keep = 10
|
||||
const initializedRunID = "OPENCODE_LOG_INITIALIZED_RUN_ID"
|
||||
|
||||
let level: Level = "INFO"
|
||||
|
||||
function shouldLog(input: Level): boolean {
|
||||
return levelPriority[input] >= levelPriority[level]
|
||||
}
|
||||
|
||||
export type Logger = {
|
||||
debug(message?: any, extra?: Record<string, any>): void
|
||||
info(message?: any, extra?: Record<string, any>): void
|
||||
error(message?: any, extra?: Record<string, any>): void
|
||||
warn(message?: any, extra?: Record<string, any>): void
|
||||
tag(key: string, value: string): Logger
|
||||
clone(): Logger
|
||||
time(
|
||||
message: string,
|
||||
extra?: Record<string, any>,
|
||||
): {
|
||||
stop(): void
|
||||
[Symbol.dispose](): void
|
||||
}
|
||||
}
|
||||
|
||||
const loggers = new Map<string, Logger>()
|
||||
|
||||
export const Default = create({ service: "default" })
|
||||
|
||||
export interface Options {
|
||||
print: boolean
|
||||
dev?: boolean
|
||||
level?: Level
|
||||
}
|
||||
|
||||
let logpath = ""
|
||||
export function file() {
|
||||
return logpath
|
||||
}
|
||||
let write = (msg: any) => {
|
||||
process.stderr.write(msg)
|
||||
return msg.length
|
||||
}
|
||||
|
||||
export async function init(options: Options) {
|
||||
if (options.level) level = options.level
|
||||
void cleanup(Global.Path.log)
|
||||
if (options.print) return
|
||||
logpath = path.join(
|
||||
Global.Path.log,
|
||||
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
|
||||
)
|
||||
const runID = process.env.OPENCODE_RUN_ID
|
||||
const shouldTruncate = !options.dev || !runID || process.env[initializedRunID] !== runID
|
||||
if (shouldTruncate) await fs.truncate(logpath).catch(() => {})
|
||||
if (options.dev && runID) process.env[initializedRunID] = runID
|
||||
const stream = createWriteStream(logpath, { flags: "a" })
|
||||
write = async (msg: any) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.write(msg, (err) => {
|
||||
if (err) reject(err)
|
||||
else resolve(msg.length)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup(dir: string) {
|
||||
const files = (
|
||||
await Glob.scan("????-??-??T??????.log", {
|
||||
cwd: dir,
|
||||
absolute: false,
|
||||
include: "file",
|
||||
}).catch(() => [])
|
||||
)
|
||||
.filter((file) => path.basename(file) === file)
|
||||
.sort()
|
||||
if (files.length <= keep) return
|
||||
|
||||
const doomed = files.slice(0, -keep)
|
||||
await Promise.all(doomed.map((file) => fs.unlink(path.join(dir, file)).catch(() => {})))
|
||||
}
|
||||
|
||||
function formatError(error: Error, depth = 0): string {
|
||||
const result = error.message
|
||||
return error.cause instanceof Error && depth < 10
|
||||
? result + " Caused by: " + formatError(error.cause, depth + 1)
|
||||
: result
|
||||
}
|
||||
|
||||
let last = Date.now()
|
||||
export function create(tags?: Record<string, any>) {
|
||||
tags = tags || {}
|
||||
|
||||
const service = tags["service"]
|
||||
if (service && typeof service === "string") {
|
||||
const cached = loggers.get(service)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
function build(message: any, extra?: Record<string, any>) {
|
||||
const prefix = Object.entries({
|
||||
...tags,
|
||||
...extra,
|
||||
})
|
||||
.filter(([_, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => {
|
||||
const prefix = `${key}=`
|
||||
if (value instanceof Error) return prefix + formatError(value)
|
||||
if (typeof value === "object") return prefix + JSON.stringify(value)
|
||||
return prefix + value
|
||||
})
|
||||
.join(" ")
|
||||
const next = new Date()
|
||||
const diff = next.getTime() - last
|
||||
last = next.getTime()
|
||||
return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
|
||||
}
|
||||
const result: Logger = {
|
||||
debug(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("DEBUG")) {
|
||||
write("DEBUG " + build(message, extra))
|
||||
}
|
||||
},
|
||||
info(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("INFO")) {
|
||||
write("INFO " + build(message, extra))
|
||||
}
|
||||
},
|
||||
error(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("ERROR")) {
|
||||
write("ERROR " + build(message, extra))
|
||||
}
|
||||
},
|
||||
warn(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("WARN")) {
|
||||
write("WARN " + build(message, extra))
|
||||
}
|
||||
},
|
||||
tag(key: string, value: string) {
|
||||
if (tags) tags[key] = value
|
||||
return result
|
||||
},
|
||||
clone() {
|
||||
return create({ ...tags })
|
||||
},
|
||||
time(message: string, extra?: Record<string, any>) {
|
||||
const now = Date.now()
|
||||
result.info(message, { status: "started", ...extra })
|
||||
function stop() {
|
||||
result.info(message, {
|
||||
status: "completed",
|
||||
duration: Date.now() - now,
|
||||
...extra,
|
||||
})
|
||||
}
|
||||
return {
|
||||
stop,
|
||||
[Symbol.dispose]() {
|
||||
stop()
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if (service && typeof service === "string") {
|
||||
loggers.set(service, result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
Vendored
-3
@@ -15,9 +15,6 @@ declare module "virtual:opencode-server" {
|
||||
export const get: typeof import("../../../opencode/dist/types/src/node").Config.get
|
||||
export type Info = import("../../../opencode/dist/types/src/node").Config.Info
|
||||
}
|
||||
export namespace Log {
|
||||
export const init: typeof import("../../../opencode/dist/types/src/node").Log.init
|
||||
}
|
||||
export namespace Database {
|
||||
export const getPath: typeof import("../../../opencode/dist/types/src/node").Database.getPath
|
||||
export const Client: typeof import("../../../opencode/dist/types/src/node").Database.Client
|
||||
|
||||
@@ -57,8 +57,8 @@ async function start(command: StartCommand) {
|
||||
ensureLoopbackNoProxy()
|
||||
useSystemCertificates()
|
||||
useEnvProxy()
|
||||
const { Database, JsonMigration, Log, Server } = await import("virtual:opencode-server")
|
||||
await Log.init({ level: "WARN" })
|
||||
process.env.OPENCODE_LOG_LEVEL = "WARN"
|
||||
const { Database, JsonMigration, Server } = await import("virtual:opencode-server")
|
||||
|
||||
if (command.needsMigration) {
|
||||
await JsonMigration.run(drizzle({ client: Database.Client().$client }), {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { AgentSideConnection } from "@agentclientprotocol/sdk"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import type {
|
||||
Event,
|
||||
EventMessagePartDelta,
|
||||
@@ -19,9 +18,6 @@ import {
|
||||
shellOutputSnapshot,
|
||||
completedToolUpdate,
|
||||
} from "./tool"
|
||||
|
||||
const log = Log.create({ service: "acp-next-event" })
|
||||
|
||||
type Connection = Pick<AgentSideConnection, "sessionUpdate">
|
||||
type GlobalEventEnvelope = {
|
||||
payload?: Event
|
||||
@@ -55,7 +51,6 @@ export class Subscription {
|
||||
this.started = true
|
||||
this.run().catch((error: unknown) => {
|
||||
if (this.abort.signal.aborted) return
|
||||
log.error("event subscription failed", { error })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -92,9 +87,7 @@ export class Subscription {
|
||||
for await (const event of events.stream) {
|
||||
if (this.abort.signal.aborted) return
|
||||
if (!event.payload) continue
|
||||
await this.handle(event.payload).catch((error: unknown) => {
|
||||
log.error("failed to handle event", { error, type: event.payload?.type })
|
||||
})
|
||||
await this.handle(event.payload).catch((error: unknown) => {})
|
||||
}
|
||||
if (!this.abort.signal.aborted) await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
}
|
||||
@@ -182,7 +175,6 @@ export class Subscription {
|
||||
)
|
||||
.then((response) => response.data)
|
||||
.catch((error: unknown) => {
|
||||
log.error("unexpected error when fetching message for delta metadata", { error, messageId, partId })
|
||||
return undefined
|
||||
})
|
||||
if (!message) return
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
type SetSessionModeResponse,
|
||||
} from "@agentclientprotocol/sdk"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import type { Message, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
|
||||
import { Context, Effect, Layer, ManagedRuntime } from "effect"
|
||||
import * as ACPNextError from "./error"
|
||||
@@ -43,8 +42,6 @@ import { Provider } from "@/provider/provider"
|
||||
import type { Command } from "@/command"
|
||||
|
||||
export const AuthMethodID = "opencode-login"
|
||||
const log = Log.create({ service: "acp-next-service" })
|
||||
|
||||
export type Error = ACPNextError.Error
|
||||
|
||||
export type Interface = {
|
||||
@@ -296,13 +293,7 @@ export function make(input: {
|
||||
yield* request(
|
||||
() => input.sdk.session.abort({ directory: removed.cwd, sessionID: params.sessionId }, { throwOnError: true }),
|
||||
"session",
|
||||
).pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
log.error("failed to abort session while closing ACP session", { error, sessionID: params.sessionId })
|
||||
}),
|
||||
),
|
||||
)
|
||||
).pipe(Effect.catch((error) => Effect.sync(() => {})))
|
||||
return {}
|
||||
})
|
||||
|
||||
@@ -477,9 +468,7 @@ function replayMessages(subscription: ACPNextEvent.Subscription | undefined, mes
|
||||
if (!subscription) return Effect.void
|
||||
return Effect.promise(async () => {
|
||||
for (const message of messages) {
|
||||
await subscription.replayMessage(message).catch((error: unknown) => {
|
||||
log.error("failed to replay ACP message", { error, messageID: message.info.id })
|
||||
})
|
||||
await subscription.replayMessage(message).catch((error: unknown) => {})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import type { AgentSideConnection, Usage } from "@agentclientprotocol/sdk"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import type { AssistantMessage as OpenCodeAssistantMessage, Message } from "@opencode-ai/sdk/v2"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Context, Effect, Layer, SynchronizedRef } from "effect"
|
||||
|
||||
const log = Log.create({ service: "acp-next-usage" })
|
||||
|
||||
export type AssistantTokenCost = Pick<OpenCodeAssistantMessage, "cost" | "tokens">
|
||||
|
||||
export type AssistantMessage = AssistantTokenCost &
|
||||
@@ -157,7 +153,6 @@ export const layer = Layer.effect(
|
||||
Effect.map((providers) => findContextLimit(providers, input.providerID, input.modelID)),
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
log.error("failed to get providers for usage context limit", { error })
|
||||
return undefined
|
||||
}),
|
||||
),
|
||||
@@ -184,7 +179,6 @@ export const layer = Layer.effect(
|
||||
const messages = yield* messageLoader.messages({ sessionID: input.sessionID, directory: input.directory }).pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
log.error("failed to fetch messages for usage update", { error })
|
||||
return undefined
|
||||
}),
|
||||
),
|
||||
@@ -213,9 +207,7 @@ export const layer = Layer.effect(
|
||||
cost: { amount: totalSessionCost(messages), currency: "USD" },
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send usage update", { error })
|
||||
}),
|
||||
.catch((error) => {}),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
type Usage,
|
||||
} from "@agentclientprotocol/sdk"
|
||||
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { pathToFileURL } from "url"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Hash } from "@opencode-ai/core/util/hash"
|
||||
@@ -57,9 +56,6 @@ type ModelOption = { modelId: string; name: string }
|
||||
const decodeTodos = Schema.decodeUnknownResult(Schema.fromJsonString(Schema.Array(Todo.Info)))
|
||||
|
||||
const DEFAULT_VARIANT_VALUE = "default"
|
||||
|
||||
const log = Log.create({ service: "acp-agent" })
|
||||
|
||||
async function getContextLimit(
|
||||
sdk: OpencodeClient,
|
||||
providerID: ProviderID,
|
||||
@@ -70,7 +66,6 @@ async function getContextLimit(
|
||||
.providers({ directory })
|
||||
.then((x) => x.data?.providers ?? [])
|
||||
.catch((error) => {
|
||||
log.error("failed to get providers for context limit", { error })
|
||||
return []
|
||||
})
|
||||
|
||||
@@ -89,7 +84,6 @@ async function sendUsageUpdate(
|
||||
.messages({ sessionID, directory }, { throwOnError: true })
|
||||
.then((x) => x.data)
|
||||
.catch((error) => {
|
||||
log.error("failed to fetch messages for usage update", { error })
|
||||
return undefined
|
||||
})
|
||||
|
||||
@@ -124,9 +118,7 @@ async function sendUsageUpdate(
|
||||
cost: { amount: totalCost, currency: "USD" },
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send usage update", { error })
|
||||
})
|
||||
.catch((error) => {})
|
||||
}
|
||||
|
||||
export function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
|
||||
@@ -166,7 +158,6 @@ export class Agent implements ACPAgent {
|
||||
this.eventStarted = true
|
||||
this.runEventSubscription().catch((error) => {
|
||||
if (this.eventAbort.signal.aborted) return
|
||||
log.error("event subscription failed", { error })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -180,9 +171,7 @@ export class Agent implements ACPAgent {
|
||||
if (this.eventAbort.signal.aborted) return
|
||||
const payload = event?.payload
|
||||
if (!payload) continue
|
||||
await this.handleEvent(payload as Event).catch((error) => {
|
||||
log.error("failed to handle event", { error, type: payload.type })
|
||||
})
|
||||
await this.handleEvent(payload as Event).catch((error) => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,11 +202,6 @@ export class Agent implements ACPAgent {
|
||||
options: this.permissionOptions,
|
||||
})
|
||||
.catch(async (error) => {
|
||||
log.error("failed to request permission from ACP", {
|
||||
error,
|
||||
permissionID: permission.id,
|
||||
sessionID: permission.sessionID,
|
||||
})
|
||||
await this.sdk.permission.reply({
|
||||
requestID: permission.id,
|
||||
reply: "reject",
|
||||
@@ -258,9 +242,7 @@ export class Agent implements ACPAgent {
|
||||
directory,
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to handle permission", { error, permissionID: permission.id })
|
||||
})
|
||||
.catch((error) => {})
|
||||
.finally(() => {
|
||||
if (this.permissionQueues.get(permission.sessionID) === next) {
|
||||
this.permissionQueues.delete(permission.sessionID)
|
||||
@@ -271,7 +253,6 @@ export class Agent implements ACPAgent {
|
||||
}
|
||||
|
||||
case "message.part.updated": {
|
||||
log.info("message part updated", { event: event.properties })
|
||||
const props = event.properties
|
||||
const part = props.part
|
||||
const session = this.sessionManager.tryGet(part.sessionID)
|
||||
@@ -306,9 +287,7 @@ export class Agent implements ACPAgent {
|
||||
rawInput: part.state.input,
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send tool in_progress to ACP", { error })
|
||||
})
|
||||
.catch((error) => {})
|
||||
return
|
||||
}
|
||||
this.shellSnapshots.set(part.callID, hash)
|
||||
@@ -335,9 +314,7 @@ export class Agent implements ACPAgent {
|
||||
...(content.length > 0 && { content }),
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send tool in_progress to ACP", { error })
|
||||
})
|
||||
.catch((error) => {})
|
||||
return
|
||||
|
||||
case "completed": {
|
||||
@@ -365,11 +342,8 @@ export class Agent implements ACPAgent {
|
||||
}),
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send session update for todo", { error })
|
||||
})
|
||||
.catch((error) => {})
|
||||
} else {
|
||||
log.error("failed to parse todo output", { error: parsedTodos.failure })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,9 +361,7 @@ export class Agent implements ACPAgent {
|
||||
rawOutput: completedToolRawOutput(part),
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send tool completed to ACP", { error })
|
||||
})
|
||||
.catch((error) => {})
|
||||
return
|
||||
}
|
||||
case "error":
|
||||
@@ -420,9 +392,7 @@ export class Agent implements ACPAgent {
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send tool error to ACP", { error })
|
||||
})
|
||||
.catch((error) => {})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -452,7 +422,6 @@ export class Agent implements ACPAgent {
|
||||
)
|
||||
.then((x) => x.data)
|
||||
.catch((error) => {
|
||||
log.error("unexpected error when fetching message", { error })
|
||||
return undefined
|
||||
})
|
||||
|
||||
@@ -474,9 +443,7 @@ export class Agent implements ACPAgent {
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send text delta to ACP", { error })
|
||||
})
|
||||
.catch((error) => {})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -493,9 +460,7 @@ export class Agent implements ACPAgent {
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send reasoning delta to ACP", { error })
|
||||
})
|
||||
.catch((error) => {})
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -503,8 +468,6 @@ export class Agent implements ACPAgent {
|
||||
}
|
||||
|
||||
async initialize(params: InitializeRequest): Promise<InitializeResponse> {
|
||||
log.info("initialize", { protocolVersion: params.protocolVersion })
|
||||
|
||||
const authMethod: AuthMethod = {
|
||||
description: "Run `opencode auth login` in the terminal",
|
||||
name: "Login with opencode",
|
||||
@@ -561,9 +524,6 @@ export class Agent implements ACPAgent {
|
||||
// Store ACP session state
|
||||
const state = await this.sessionManager.create(params.cwd, params.mcpServers, model)
|
||||
const sessionId = state.id
|
||||
|
||||
log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length })
|
||||
|
||||
const load = await this.loadSessionMode({
|
||||
cwd: directory,
|
||||
mcpServers: params.mcpServers,
|
||||
@@ -600,9 +560,6 @@ export class Agent implements ACPAgent {
|
||||
|
||||
const messages = await this.loadSessionMessages(directory, sessionId)
|
||||
this.restoreSessionStateFromMessages(sessionId, messages)
|
||||
|
||||
log.info("load_session", { sessionId, mcpServers: params.mcpServers.length })
|
||||
|
||||
const result = await this.loadSessionMode({
|
||||
cwd: directory,
|
||||
mcpServers: params.mcpServers,
|
||||
@@ -610,7 +567,6 @@ export class Agent implements ACPAgent {
|
||||
})
|
||||
|
||||
for (const msg of messages ?? []) {
|
||||
log.debug("replay message", msg)
|
||||
await this.processMessage(msg)
|
||||
}
|
||||
|
||||
@@ -699,9 +655,6 @@ export class Agent implements ACPAgent {
|
||||
|
||||
const messages = await this.loadSessionMessages(directory, sessionId)
|
||||
this.restoreSessionStateFromMessages(sessionId, messages)
|
||||
|
||||
log.info("fork_session", { sessionId, mcpServers: mcpServers.length })
|
||||
|
||||
const mode = await this.loadSessionMode({
|
||||
cwd: directory,
|
||||
mcpServers,
|
||||
@@ -709,7 +662,6 @@ export class Agent implements ACPAgent {
|
||||
})
|
||||
|
||||
for (const msg of messages ?? []) {
|
||||
log.debug("replay message", msg)
|
||||
await this.processMessage(msg)
|
||||
}
|
||||
|
||||
@@ -738,9 +690,6 @@ export class Agent implements ACPAgent {
|
||||
|
||||
const messages = await this.loadSessionMessages(directory, sessionId, 20)
|
||||
this.restoreSessionStateFromMessages(sessionId, messages)
|
||||
|
||||
log.info("resume_session", { sessionId, mcpServers: mcpServers.length })
|
||||
|
||||
const result = await this.loadSessionMode({
|
||||
cwd: directory,
|
||||
mcpServers,
|
||||
@@ -773,17 +722,13 @@ export class Agent implements ACPAgent {
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
.catch((error) => {
|
||||
log.error("failed to abort session while closing ACP session", { error, sessionID: params.sessionId })
|
||||
})
|
||||
.catch((error) => {})
|
||||
|
||||
this.permissionQueues.delete(params.sessionId)
|
||||
log.info("close_session", { sessionId: params.sessionId })
|
||||
return {}
|
||||
}
|
||||
|
||||
private async processMessage(message: SessionMessageResponse) {
|
||||
log.debug("process message", message)
|
||||
if (message.info.role !== "assistant" && message.info.role !== "user") return
|
||||
const sessionId = message.info.sessionID
|
||||
|
||||
@@ -820,9 +765,7 @@ export class Agent implements ACPAgent {
|
||||
...(runningContent.length > 0 && { content: runningContent }),
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send tool in_progress to ACP", { error: err })
|
||||
})
|
||||
.catch((err) => {})
|
||||
break
|
||||
case "completed":
|
||||
this.toolStarts.delete(part.callID)
|
||||
@@ -849,11 +792,8 @@ export class Agent implements ACPAgent {
|
||||
}),
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send session update for todo", { error: err })
|
||||
})
|
||||
.catch((err) => {})
|
||||
} else {
|
||||
log.error("failed to parse todo output", { error: parsedTodos.failure })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -871,9 +811,7 @@ export class Agent implements ACPAgent {
|
||||
rawOutput: completedToolRawOutput(part),
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send tool completed to ACP", { error: err })
|
||||
})
|
||||
.catch((err) => {})
|
||||
break
|
||||
case "error":
|
||||
this.toolStarts.delete(part.callID)
|
||||
@@ -903,9 +841,7 @@ export class Agent implements ACPAgent {
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send tool error to ACP", { error: err })
|
||||
})
|
||||
.catch((err) => {})
|
||||
break
|
||||
}
|
||||
} else if (part.type === "text") {
|
||||
@@ -924,9 +860,7 @@ export class Agent implements ACPAgent {
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send text to ACP", { error: err })
|
||||
})
|
||||
.catch((err) => {})
|
||||
}
|
||||
} else if (part.type === "file") {
|
||||
// Replay file attachments as appropriate ACP content blocks.
|
||||
@@ -952,9 +886,7 @@ export class Agent implements ACPAgent {
|
||||
content: { type: "resource_link", uri: url, name: filename, mimeType: mime },
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send resource_link to ACP", { error: err })
|
||||
})
|
||||
.catch((err) => {})
|
||||
} else if (url.startsWith("data:")) {
|
||||
// Embedded content - parse data URL and send as appropriate block type
|
||||
const base64Match = url.match(/^data:([^;]+);base64,(.*)$/)
|
||||
@@ -979,9 +911,7 @@ export class Agent implements ACPAgent {
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send image to ACP", { error: err })
|
||||
})
|
||||
.catch((err) => {})
|
||||
} else {
|
||||
// Non-image: text types get decoded, binary types stay as blob
|
||||
const isText = effectiveMime.startsWith("text/") || effectiveMime === "application/json"
|
||||
@@ -1003,9 +933,7 @@ export class Agent implements ACPAgent {
|
||||
content: { type: "resource", resource },
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send resource to ACP", { error: err })
|
||||
})
|
||||
.catch((err) => {})
|
||||
}
|
||||
}
|
||||
// URLs that don't match file:// or data: are skipped (unsupported)
|
||||
@@ -1023,9 +951,7 @@ export class Agent implements ACPAgent {
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send reasoning to ACP", { error: err })
|
||||
})
|
||||
.catch((err) => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1055,9 +981,7 @@ export class Agent implements ACPAgent {
|
||||
rawInput: {},
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send tool pending to ACP", { error })
|
||||
})
|
||||
.catch((error) => {})
|
||||
}
|
||||
|
||||
private async loadAvailableModes(directory: string): Promise<ModeOption[]> {
|
||||
@@ -1176,9 +1100,7 @@ export class Agent implements ACPAgent {
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
.catch((error) => {
|
||||
log.error("failed to add mcp server", { name: key, error })
|
||||
})
|
||||
.catch((error) => {})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1400,9 +1322,6 @@ export class Agent implements ACPAgent {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.info("parts", { parts })
|
||||
|
||||
const cmd = (() => {
|
||||
const text = parts
|
||||
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
||||
@@ -1521,7 +1440,6 @@ export class Agent implements ACPAgent {
|
||||
)
|
||||
.then((x) => x.data)
|
||||
.catch((error) => {
|
||||
log.error("unexpected error when fetching message", { error })
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
@@ -1673,7 +1591,6 @@ async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ provider
|
||||
return Provider.parseModel(cfg.model)
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to load user config for default model", { error })
|
||||
return undefined
|
||||
})
|
||||
|
||||
@@ -1681,7 +1598,6 @@ async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ provider
|
||||
.providers({ directory }, { throwOnError: true })
|
||||
.then((x) => x.data?.providers ?? [])
|
||||
.catch((error) => {
|
||||
log.error("failed to list providers for default model", { error })
|
||||
return []
|
||||
})
|
||||
|
||||
@@ -1728,7 +1644,6 @@ async function lastUsedModel(
|
||||
.list({ directory, roots: true, limit: 1 }, { throwOnError: true })
|
||||
.then((x) => x.data?.[0])
|
||||
.catch((error) => {
|
||||
log.error("failed to list sessions for default model", { error })
|
||||
return undefined
|
||||
})
|
||||
if (!session) return
|
||||
@@ -1737,7 +1652,6 @@ async function lastUsedModel(
|
||||
.messages({ sessionID: session.id, directory, limit: 20 }, { throwOnError: true })
|
||||
.then((x) => x.data?.findLast((message) => message.info.role === "user")?.info)
|
||||
.catch((error) => {
|
||||
log.error("failed to load session messages for default model", { error, sessionID: session.id })
|
||||
return undefined
|
||||
})
|
||||
if (lastUser?.role !== "user") return
|
||||
@@ -1792,7 +1706,6 @@ function parseUri(
|
||||
function getNewContent(fileOriginal: string, unifiedDiff: string): string | undefined {
|
||||
const result = applyPatch(fileOriginal, unifiedDiff)
|
||||
if (result === false) {
|
||||
log.error("Failed to apply unified diff (context mismatch)")
|
||||
return undefined
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { RequestError, type McpServer } from "@agentclientprotocol/sdk"
|
||||
import type { ACPSessionState } from "./types"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
|
||||
const log = Log.create({ service: "acp-session-manager" })
|
||||
|
||||
export class ACPSessionManager {
|
||||
private sessions = new Map<string, ACPSessionState>()
|
||||
private sdk: OpencodeClient
|
||||
@@ -37,8 +33,6 @@ export class ACPSessionManager {
|
||||
createdAt: new Date(),
|
||||
model: resolvedModel,
|
||||
}
|
||||
log.info("creating_session", { state })
|
||||
|
||||
this.sessions.set(sessionId, state)
|
||||
return state
|
||||
}
|
||||
@@ -68,8 +62,6 @@ export class ACPSessionManager {
|
||||
createdAt: new Date(session.time.created),
|
||||
model: resolvedModel,
|
||||
}
|
||||
log.info("loading_session", { state })
|
||||
|
||||
this.sessions.set(sessionId, state)
|
||||
return state
|
||||
}
|
||||
@@ -77,7 +69,6 @@ export class ACPSessionManager {
|
||||
get(sessionId: string): ACPSessionState {
|
||||
const session = this.sessions.get(sessionId)
|
||||
if (!session) {
|
||||
log.error("session not found", { sessionId })
|
||||
throw RequestError.invalidParams(JSON.stringify({ error: `Session not found: ${sessionId}` }))
|
||||
}
|
||||
return session
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream, Schema } from "effect"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { BusEvent } from "./bus-event"
|
||||
import { GlobalBus } from "./global"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
@@ -9,9 +8,6 @@ import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { Identifier } from "@/id/id"
|
||||
import type { InstanceContext } from "@/project/instance-context"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
|
||||
const log = Log.create({ service: "bus" })
|
||||
|
||||
type BusProperties<D extends BusEvent.Definition<string, Schema.Top>> = Schema.Schema.Type<D["properties"]>
|
||||
|
||||
export const InstanceDisposed = BusEvent.define(
|
||||
@@ -101,7 +97,7 @@ export const layer = Layer.effect(
|
||||
return Effect.gen(function* () {
|
||||
const s = yield* InstanceState.get(state)
|
||||
const payload: Payload = { id: options?.id ?? createID(), type: def.type, properties }
|
||||
log.info("publishing", { type: def.type })
|
||||
yield* Effect.logInfo("publishing").pipe(Effect.annotateLogs({ service: "bus", ...{ type: def.type } }))
|
||||
|
||||
const ps = s.typed.get(def.type)
|
||||
if (ps) yield* PubSub.publish(ps, payload)
|
||||
@@ -124,26 +120,30 @@ export const layer = Layer.effect(
|
||||
def: D,
|
||||
): Effect.Effect<Stream.Stream<Payload<D>>, never, Scope.Scope> =>
|
||||
Effect.gen(function* () {
|
||||
log.info("subscribing", { type: def.type })
|
||||
yield* Effect.logInfo("subscribing").pipe(Effect.annotateLogs({ service: "bus", ...{ type: def.type } }))
|
||||
const s = yield* InstanceState.get(state)
|
||||
const ps = yield* getOrCreate(s, def)
|
||||
const subscription = yield* PubSub.subscribe(ps)
|
||||
yield* Effect.addFinalizer(() => Effect.sync(() => log.info("unsubscribing", { type: def.type })))
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.logInfo("unsubscribing").pipe(Effect.annotateLogs({ service: "bus", ...{ type: def.type } })),
|
||||
)
|
||||
return Stream.fromSubscription(subscription)
|
||||
})
|
||||
|
||||
const subscribeAll = (): Effect.Effect<Stream.Stream<Payload>, never, Scope.Scope> =>
|
||||
Effect.gen(function* () {
|
||||
log.info("subscribing", { type: "*" })
|
||||
yield* Effect.logInfo("subscribing").pipe(Effect.annotateLogs({ service: "bus", ...{ type: "*" } }))
|
||||
const s = yield* InstanceState.get(state)
|
||||
const subscription = yield* PubSub.subscribe(s.wildcard)
|
||||
yield* Effect.addFinalizer(() => Effect.sync(() => log.info("unsubscribing", { type: "*" })))
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.logInfo("unsubscribing").pipe(Effect.annotateLogs({ service: "bus", ...{ type: "*" } })),
|
||||
)
|
||||
return Stream.fromSubscription(subscription)
|
||||
})
|
||||
|
||||
function on<T>(pubsub: PubSub.PubSub<T>, type: string, callback: (event: T) => unknown) {
|
||||
return Effect.gen(function* () {
|
||||
log.info("subscribing", { type })
|
||||
yield* Effect.logInfo("subscribing").pipe(Effect.annotateLogs({ service: "bus", ...{ type } }))
|
||||
const bridge = yield* EffectBridge.make()
|
||||
const scope = yield* Scope.make()
|
||||
const subscription = yield* Scope.provide(scope)(PubSub.subscribe(pubsub))
|
||||
@@ -153,18 +153,25 @@ export const layer = Layer.effect(
|
||||
Stream.runForEach((msg) =>
|
||||
Effect.tryPromise({
|
||||
try: () => Promise.resolve().then(() => callback(msg)),
|
||||
catch: (cause) => {
|
||||
log.error("subscriber failed", { type, cause })
|
||||
},
|
||||
}).pipe(Effect.ignore),
|
||||
catch: (cause) => cause,
|
||||
}).pipe(
|
||||
Effect.tapError((cause) =>
|
||||
Effect.logError("subscriber failed").pipe(Effect.annotateLogs({ service: "bus", ...{ type, cause } })),
|
||||
),
|
||||
Effect.ignore,
|
||||
),
|
||||
),
|
||||
Effect.forkScoped,
|
||||
),
|
||||
)
|
||||
|
||||
return () => {
|
||||
log.info("unsubscribing", { type })
|
||||
bridge.fork(Scope.close(scope, Exit.void))
|
||||
bridge.fork(
|
||||
Effect.logInfo("unsubscribing").pipe(
|
||||
Effect.annotateLogs({ service: "bus", ...{ type } }),
|
||||
Effect.andThen(Scope.close(scope, Exit.void)),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Effect } from "effect"
|
||||
import { effectCmd } from "../effect-cmd"
|
||||
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
|
||||
@@ -9,9 +8,6 @@ import { ServerAuth } from "@/server/auth"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
|
||||
const log = Log.create({ service: "acp-command" })
|
||||
|
||||
export const AcpCommand = effectCmd({
|
||||
command: "acp",
|
||||
describe: "start ACP (Agent Client Protocol) server",
|
||||
@@ -63,7 +59,7 @@ export const AcpCommand = effectCmd({
|
||||
return agent.create(conn, { sdk })
|
||||
}, stream)
|
||||
|
||||
log.info("setup connection")
|
||||
yield* Effect.logInfo("setup connection").pipe(Effect.annotateLogs({ service: "acp-command" }))
|
||||
process.stdin.resume()
|
||||
yield* Effect.promise(
|
||||
() =>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { LSP } from "@/lsp/lsp"
|
||||
import { Effect } from "effect"
|
||||
import { effectCmd } from "../../effect-cmd"
|
||||
import { cmd } from "../cmd"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { EOL } from "os"
|
||||
|
||||
export const LSPCommand = cmd({
|
||||
@@ -33,7 +32,6 @@ export const SymbolsCommand = effectCmd({
|
||||
describe: "search workspace symbols",
|
||||
builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }),
|
||||
handler: Effect.fn("Cli.debug.lsp.symbols")(function* (args) {
|
||||
using _ = Log.Default.time("symbols")
|
||||
const results = yield* LSP.Service.use((lsp) => lsp.workspaceSymbol(args.query))
|
||||
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
||||
}),
|
||||
@@ -44,7 +42,6 @@ export const DocumentSymbolsCommand = effectCmd({
|
||||
describe: "get symbols from a document",
|
||||
builder: (yargs) => yargs.positional("uri", { type: "string", demandOption: true }),
|
||||
handler: Effect.fn("Cli.debug.lsp.documentSymbols")(function* (args) {
|
||||
using _ = Log.Default.time("document-symbols")
|
||||
const results = yield* LSP.Service.use((lsp) => lsp.documentSymbol(args.uri))
|
||||
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { EOL } from "os"
|
||||
import { Project } from "@/project/project"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { cmd } from "../cmd"
|
||||
|
||||
export const ScrapCommand = cmd({
|
||||
@@ -8,9 +7,7 @@ export const ScrapCommand = cmd({
|
||||
describe: "list all known projects",
|
||||
builder: (yargs) => yargs,
|
||||
async handler() {
|
||||
const timer = Log.Default.time("scrap")
|
||||
const list = await Project.list()
|
||||
process.stdout.write(JSON.stringify(list, null, 2) + EOL)
|
||||
timer.stop()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Dev-only JSONL event trace for direct interactive mode.
|
||||
//
|
||||
// Enable with OPENCODE_DIRECT_TRACE=1. Writes one JSON line per event to
|
||||
// ~/.local/share/opencode/log/direct/<timestamp>-<pid>.jsonl. Also writes
|
||||
// ~/.local/state/opencode/direct/<timestamp>-<pid>.jsonl. Also writes
|
||||
// a latest.json pointer so you can quickly find the most recent trace.
|
||||
//
|
||||
// The trace captures the full closed loop: outbound prompts, inbound SDK
|
||||
|
||||
@@ -19,8 +19,6 @@ import permissionSoundPath from "@opencode-ai/ui/audio/staplebops-06.mp3" with {
|
||||
import errorSoundPath from "@opencode-ai/ui/audio/nope-03.mp3" with { type: "file" }
|
||||
import doneSoundPath from "@opencode-ai/ui/audio/bip-bop-01.mp3" with { type: "file" }
|
||||
import subagentDoneSoundPath from "@opencode-ai/ui/audio/yup-01.mp3" with { type: "file" }
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
type FocusState = "unknown" | "focused" | "blurred"
|
||||
|
||||
type AttentionRenderer = {
|
||||
@@ -37,9 +35,6 @@ type RegisteredSoundPack = TuiAttentionSoundPack & {
|
||||
type TuiAttentionHost = TuiAttention & {
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
const log = Log.create({ service: "tui.attention" })
|
||||
|
||||
const DEFAULT_TITLE = "opencode"
|
||||
const DEFAULT_PACK_ID = "opencode.default"
|
||||
const KV_SOUND_PACK = "attention_sound_pack"
|
||||
@@ -154,7 +149,6 @@ export function createTuiAttention(input: {
|
||||
try {
|
||||
for (const file of soundCandidates(name)) {
|
||||
const current = await audio.loadSoundFile(file).catch((error) => {
|
||||
log.debug("failed to load attention sound", { file, error })
|
||||
return null
|
||||
})
|
||||
if (disposed) return false
|
||||
@@ -163,7 +157,6 @@ export function createTuiAttention(input: {
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
log.debug("failed to play attention sound", { error })
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -189,7 +182,6 @@ export function createTuiAttention(input: {
|
||||
normalizeText(request.title, DEFAULT_TITLE, TITLE_LIMIT),
|
||||
)
|
||||
} catch (error) {
|
||||
log.debug("failed to trigger attention notification", { error })
|
||||
return false
|
||||
}
|
||||
})()
|
||||
@@ -212,7 +204,6 @@ export function createTuiAttention(input: {
|
||||
sound,
|
||||
}
|
||||
} catch (error) {
|
||||
log.debug("failed to handle attention notification", { error })
|
||||
return {
|
||||
ok: false,
|
||||
notification: false,
|
||||
|
||||
@@ -6,11 +6,7 @@ import { DiffStyle, ScrollAcceleration, ScrollSpeed } from "./tui-schema"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import * as ConfigPaths from "@/config/paths"
|
||||
|
||||
const log = Log.create({ service: "tui.migrate" })
|
||||
|
||||
const TUI_SCHEMA_URL = "https://opencode.ai/tui.json"
|
||||
|
||||
const decodeTheme = Schema.decodeUnknownOption(Schema.String)
|
||||
@@ -33,7 +29,6 @@ export async function migrateTuiConfig(input: MigrateInput) {
|
||||
const opencode = await opencodeFiles(input)
|
||||
for (const file of opencode) {
|
||||
const source = await Filesystem.readText(file).catch((error) => {
|
||||
log.warn("failed to read config for tui migration", { path: file, error })
|
||||
return undefined
|
||||
})
|
||||
if (!source) continue
|
||||
@@ -66,17 +61,14 @@ export async function migrateTuiConfig(input: MigrateInput) {
|
||||
const wrote = await Filesystem.write(target, JSON.stringify(payload, null, 2))
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
log.warn("failed to write tui migration target", { from: file, to: target, error })
|
||||
return false
|
||||
})
|
||||
if (!wrote) continue
|
||||
|
||||
const stripped = await backupAndStripLegacy(file, source)
|
||||
if (!stripped) {
|
||||
log.warn("tui config migrated but source file was not stripped", { from: file, to: target })
|
||||
continue
|
||||
}
|
||||
log.info("migrated tui config", { from: file, to: target })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +99,6 @@ async function backupAndStripLegacy(file: string, source: string) {
|
||||
: await Filesystem.write(backup, source)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
log.warn("failed to backup source config during tui migration", { path: file, backup, error })
|
||||
return false
|
||||
})
|
||||
if (!backed) return false
|
||||
@@ -125,11 +116,9 @@ async function backupAndStripLegacy(file: string, source: string) {
|
||||
|
||||
return Filesystem.write(file, text)
|
||||
.then(() => {
|
||||
log.info("stripped tui keys from server config", { path: file, backup })
|
||||
return true
|
||||
})
|
||||
.catch((error) => {
|
||||
log.warn("failed to strip legacy tui keys from server config", { path: file, backup, error })
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,15 +18,11 @@ import { TuiKeybind } from "./keybind"
|
||||
import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { makeRuntime } from "@opencode-ai/core/effect/runtime"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { ConfigVariable } from "@/config/variable"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import type { DeepMutable } from "@opencode-ai/core/schema"
|
||||
import type { TuiAttentionSoundName } from "@opencode-ai/plugin/tui"
|
||||
import { FormatError, FormatUnknownError } from "@/cli/error"
|
||||
|
||||
const log = Log.create({ service: "tui.config" })
|
||||
|
||||
export const Info = TuiInfo
|
||||
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>>
|
||||
|
||||
@@ -84,12 +80,6 @@ function dropUnknownKeybinds(input: Record<string, unknown>, configFilepath: str
|
||||
|
||||
const invalid = TuiKeybind.unknownKeys(input.keybinds)
|
||||
if (!invalid.length) return input
|
||||
|
||||
log.warn("ignored unknown tui keybinds", {
|
||||
path: configFilepath,
|
||||
keybinds: invalid,
|
||||
hint: "Remove these entries or rename them to keys from the tui.json schema.",
|
||||
})
|
||||
return {
|
||||
...input,
|
||||
keybinds: Object.fromEntries(Object.entries(input.keybinds).filter(([key]) => !invalid.includes(key))),
|
||||
@@ -138,10 +128,6 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
|
||||
Effect.sync(() => {
|
||||
const error = Cause.squash(cause)
|
||||
const reason = FormatError(error) ?? FormatUnknownError(error)
|
||||
log.warn("skipping invalid tui config", {
|
||||
path: configFilepath,
|
||||
reason,
|
||||
})
|
||||
return {} as Info
|
||||
}),
|
||||
),
|
||||
@@ -157,16 +143,14 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
|
||||
Effect.sync(() => {
|
||||
const error = Cause.squash(cause)
|
||||
const reason = FormatError(error) ?? FormatUnknownError(error)
|
||||
log.warn("failed to read tui config", {
|
||||
path: filepath,
|
||||
reason,
|
||||
})
|
||||
return undefined
|
||||
}),
|
||||
),
|
||||
)
|
||||
if (!text) return {} as Info
|
||||
log.info("loading tui config", { path: filepath })
|
||||
yield* Effect.logInfo("loading tui config").pipe(
|
||||
Effect.annotateLogs({ service: "tui.config", ...{ path: filepath } }),
|
||||
)
|
||||
return yield* load(text, filepath)
|
||||
})
|
||||
|
||||
@@ -175,7 +159,9 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
|
||||
const data = yield* loadFile(file)
|
||||
if (Object.keys(data).length) {
|
||||
appliedOrder += 1
|
||||
log.info("applying tui config", { path: file, order: appliedOrder })
|
||||
yield* Effect.logInfo("applying tui config").pipe(
|
||||
Effect.annotateLogs({ service: "tui.config", ...{ path: file, order: appliedOrder } }),
|
||||
)
|
||||
}
|
||||
acc.result = mergeDeep(acc.result, data)
|
||||
if (!data.plugin?.length) return
|
||||
@@ -210,7 +196,9 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
|
||||
if (Flag.OPENCODE_TUI_CONFIG) {
|
||||
const configFile = Flag.OPENCODE_TUI_CONFIG
|
||||
yield* mergeFile(acc, configFile)
|
||||
log.debug("loaded custom tui config", { path: configFile })
|
||||
yield* Effect.logDebug("loaded custom tui config").pipe(
|
||||
Effect.annotateLogs({ service: "tui.config", ...{ path: configFile } }),
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Project tui files, applied root-first so the closest file wins.
|
||||
|
||||
@@ -28,7 +28,6 @@ import type { Snapshot } from "@/snapshot"
|
||||
import { useExit } from "./exit"
|
||||
import { useArgs } from "./args"
|
||||
import { batch, onMount } from "solid-js"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { emptyConsoleState, type ConsoleState } from "@/config/console-state"
|
||||
import path from "path"
|
||||
import { useKV } from "./kv"
|
||||
@@ -465,11 +464,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
})
|
||||
})
|
||||
.catch(async (e) => {
|
||||
Log.Default.error("tui bootstrap failed", {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
name: e instanceof Error ? e.name : undefined,
|
||||
stack: e instanceof Error ? e.stack : undefined,
|
||||
})
|
||||
if (fatal) {
|
||||
await exit(e)
|
||||
} else {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { errorData, errorMessage } from "@/util/error"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { resolveAttentionSoundPaths } from "../config/tui-schema"
|
||||
@@ -116,8 +115,6 @@ type RuntimeState = {
|
||||
pending: Map<string, ConfigPlugin.Origin>
|
||||
dispose_timeout_ms: number
|
||||
}
|
||||
|
||||
const log = Log.create({ service: "tui.plugin" })
|
||||
const DISPOSE_TIMEOUT_MS = 5000
|
||||
const KV_KEY = "plugin_enabled"
|
||||
const EMPTY_TUI: TuiPluginModule = {
|
||||
@@ -126,19 +123,16 @@ const EMPTY_TUI: TuiPluginModule = {
|
||||
|
||||
function fail(message: string, data: Record<string, unknown>) {
|
||||
if (!("error" in data)) {
|
||||
log.error(message, data)
|
||||
console.error(`[tui.plugin] ${message}`, data)
|
||||
return
|
||||
}
|
||||
|
||||
const text = `${message}: ${errorMessage(data.error)}`
|
||||
const next = { ...data, error: errorData(data.error) }
|
||||
log.error(text, next)
|
||||
console.error(`[tui.plugin] ${text}`, next)
|
||||
}
|
||||
|
||||
function warn(message: string, data: Record<string, unknown>) {
|
||||
log.warn(message, data)
|
||||
console.warn(`[tui.plugin] ${message}`, data)
|
||||
}
|
||||
|
||||
@@ -273,15 +267,7 @@ function createThemeInstaller(
|
||||
await Flock.withLock(`tui-theme:${dest}`, async () => {
|
||||
const save = async () => {
|
||||
plugin.themes[name] = info
|
||||
await PluginMeta.setTheme(plugin.id, name, info).catch((error) => {
|
||||
log.warn("failed to track tui plugin theme", {
|
||||
path: spec,
|
||||
id: plugin.id,
|
||||
theme: src,
|
||||
dest,
|
||||
error,
|
||||
})
|
||||
})
|
||||
await PluginMeta.setTheme(plugin.id, name, info).catch((error) => {})
|
||||
}
|
||||
|
||||
const exists = hasTheme(name)
|
||||
@@ -297,7 +283,6 @@ function createThemeInstaller(
|
||||
}
|
||||
|
||||
const text = await Filesystem.readText(src).catch((error) => {
|
||||
log.warn("failed to read tui plugin theme", { path: spec, theme: src, error })
|
||||
return
|
||||
})
|
||||
if (text === undefined) return
|
||||
@@ -306,27 +291,21 @@ function createThemeInstaller(
|
||||
const data = await Promise.resolve(text)
|
||||
.then((x) => JSON.parse(x))
|
||||
.catch((error) => {
|
||||
log.warn("failed to parse tui plugin theme", { path: spec, theme: src, error })
|
||||
return fail
|
||||
})
|
||||
if (data === fail) return
|
||||
|
||||
if (!isTheme(data)) {
|
||||
log.warn("invalid tui plugin theme", { path: spec, theme: src })
|
||||
return
|
||||
}
|
||||
|
||||
if (exists || !(await Filesystem.exists(dest))) {
|
||||
await Filesystem.write(dest, text).catch((error) => {
|
||||
log.warn("failed to persist tui plugin theme", { path: spec, theme: src, dest, error })
|
||||
})
|
||||
await Filesystem.write(dest, text).catch((error) => {})
|
||||
}
|
||||
|
||||
upsertTheme(name, data)
|
||||
await save()
|
||||
}).catch((error) => {
|
||||
log.warn("failed to lock tui plugin theme install", { path: spec, theme: src, dest, error })
|
||||
})
|
||||
}).catch((error) => {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,9 +668,7 @@ async function resolveExternalPlugins(list: ConfigPlugin.Origin[], wait: () => P
|
||||
items: list,
|
||||
kind: "tui",
|
||||
wait: async () => {
|
||||
await wait().catch((error) => {
|
||||
log.warn("failed waiting for tui plugin dependencies", { error })
|
||||
})
|
||||
await wait().catch((error) => {})
|
||||
},
|
||||
finish: async (loaded, origin, retry) => {
|
||||
const mod = await Promise.resolve()
|
||||
@@ -762,9 +739,7 @@ async function resolveExternalPlugins(list: ConfigPlugin.Origin[], wait: () => P
|
||||
}
|
||||
},
|
||||
report: {
|
||||
start(candidate, retry) {
|
||||
log.info("loading tui plugin", { path: candidate.plan.spec, retry })
|
||||
},
|
||||
start(candidate, retry) {},
|
||||
missing(candidate, retry, message) {
|
||||
warn("tui plugin has no entrypoint", { path: candidate.plan.spec, retry, message })
|
||||
},
|
||||
@@ -798,7 +773,6 @@ async function addExternalPluginEntries(state: RuntimeState, ready: PluginLoad[]
|
||||
id: item.id,
|
||||
})),
|
||||
).catch((error) => {
|
||||
log.warn("failed to track tui plugins", { error })
|
||||
return undefined
|
||||
})
|
||||
|
||||
@@ -809,14 +783,6 @@ async function addExternalPluginEntries(state: RuntimeState, ready: PluginLoad[]
|
||||
if (!entry) continue
|
||||
const hit = meta?.[i]
|
||||
if (hit && hit.state !== "same") {
|
||||
log.info("tui plugin metadata updated", {
|
||||
path: entry.spec,
|
||||
retry: entry.retry,
|
||||
state: hit.state,
|
||||
source: hit.entry.source,
|
||||
version: hit.entry.version,
|
||||
modified: hit.entry.modified,
|
||||
})
|
||||
}
|
||||
|
||||
const info = createMeta(entry.source, entry.spec, entry.target, hit, entry.id)
|
||||
@@ -1093,11 +1059,9 @@ async function load(input: { api: Api; config: TuiConfig.Resolved; dispose?: ()
|
||||
)
|
||||
const records = Flag.OPENCODE_PURE ? [] : (config.plugin_origins ?? [])
|
||||
if (Flag.OPENCODE_PURE && config.plugin_origins?.length) {
|
||||
log.info("skipping external tui plugins in pure mode", { count: config.plugin_origins.length })
|
||||
}
|
||||
|
||||
for (const item of internalTuiPlugins(flags)) {
|
||||
log.info("loading internal tui plugin", { id: item.id })
|
||||
const entry = loadInternalPlugin(item)
|
||||
const meta = createMeta(entry.source, entry.spec, entry.target, undefined, entry.id)
|
||||
addPluginEntry(next, {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { type rpc } from "./worker"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { UI } from "@/cli/ui"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { errorMessage } from "@/util/error"
|
||||
import { withTimeout } from "@/util/timeout"
|
||||
import { withNetworkOptions, resolveNetworkOptionsNoConfig } from "@/cli/network"
|
||||
@@ -146,26 +145,12 @@ export const TuiThreadCommand = cmd({
|
||||
const worker = new Worker(file, {
|
||||
env,
|
||||
})
|
||||
worker.onerror = (e) => {
|
||||
Log.Default.error("thread error", {
|
||||
message: e.message,
|
||||
filename: e.filename,
|
||||
lineno: e.lineno,
|
||||
colno: e.colno,
|
||||
error: e.error,
|
||||
})
|
||||
}
|
||||
worker.onerror = () => {}
|
||||
|
||||
const client = Rpc.client<typeof rpc>(worker)
|
||||
const error = (e: unknown) => {
|
||||
Log.Default.error("process error", { error: errorMessage(e) })
|
||||
}
|
||||
const error = () => {}
|
||||
const reload = () => {
|
||||
client.call("reload", undefined).catch((err) => {
|
||||
Log.Default.warn("worker reload failed", {
|
||||
error: errorMessage(err),
|
||||
})
|
||||
})
|
||||
client.call("reload", undefined).catch(() => {})
|
||||
}
|
||||
process.on("uncaughtException", error)
|
||||
process.on("unhandledRejection", error)
|
||||
@@ -178,11 +163,7 @@ export const TuiThreadCommand = cmd({
|
||||
process.off("uncaughtException", error)
|
||||
process.off("unhandledRejection", error)
|
||||
process.off("SIGUSR2", reload)
|
||||
await withTimeout(client.call("shutdown", undefined), 5000).catch((error) => {
|
||||
Log.Default.warn("worker shutdown failed", {
|
||||
error: errorMessage(error),
|
||||
})
|
||||
})
|
||||
await withTimeout(client.call("shutdown", undefined), 5000).catch(() => {})
|
||||
worker.terminate()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { Audio, type AudioErrorContext, type AudioPlayOptions, type AudioSound, type AudioVoice } from "@opentui/core"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
const log = Log.create({ service: "tui.audio" })
|
||||
|
||||
let audio: Audio | null | undefined
|
||||
const sounds = new Map<string, Promise<AudioSound | null>>()
|
||||
|
||||
@@ -10,13 +6,10 @@ function getAudio() {
|
||||
if (audio !== undefined) return audio
|
||||
try {
|
||||
const next = Audio.create({ autoStart: false })
|
||||
next.on("error", (error: Error, context: AudioErrorContext) => {
|
||||
log.debug("tui audio error", { error, context })
|
||||
})
|
||||
next.on("error", (error: Error, context: AudioErrorContext) => {})
|
||||
audio = next
|
||||
return next
|
||||
} catch (error) {
|
||||
log.debug("failed to create tui audio", { error })
|
||||
audio = null
|
||||
return null
|
||||
}
|
||||
@@ -31,7 +24,6 @@ export function loadSoundFile(file: string) {
|
||||
.bytes()
|
||||
.then((bytes) => current.loadSound(bytes))
|
||||
.catch((error) => {
|
||||
log.debug("failed to load tui sound", { file, error })
|
||||
return null
|
||||
})
|
||||
sounds.set(file, task)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Installation } from "@/installation"
|
||||
import { Server } from "@/server/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { InstanceRuntime } from "@/project/instance-runtime"
|
||||
import { Rpc } from "@/util/rpc"
|
||||
import { upgrade } from "@/cli/upgrade"
|
||||
@@ -15,30 +14,10 @@ import { Effect } from "effect"
|
||||
import { disposeAllInstancesAndEmitGlobalDisposed } from "@/server/global-lifecycle"
|
||||
|
||||
ensureProcessMetadata("worker")
|
||||
|
||||
await Log.init({
|
||||
print: process.argv.includes("--print-logs"),
|
||||
dev: Installation.isLocal(),
|
||||
level: (() => {
|
||||
if (Installation.isLocal()) return "DEBUG"
|
||||
return "INFO"
|
||||
})(),
|
||||
})
|
||||
if (!process.env.OPENCODE_LOG_LEVEL) process.env.OPENCODE_LOG_LEVEL = Installation.isLocal() ? "DEBUG" : "INFO"
|
||||
|
||||
Heap.start()
|
||||
|
||||
process.on("unhandledRejection", (e) => {
|
||||
Log.Default.error("rejection", {
|
||||
e: e instanceof Error ? e.message : e,
|
||||
})
|
||||
})
|
||||
|
||||
process.on("uncaughtException", (e) => {
|
||||
Log.Default.error("exception", {
|
||||
e: e instanceof Error ? e.message : e,
|
||||
})
|
||||
})
|
||||
|
||||
// Subscribe to global events and forward them via RPC
|
||||
GlobalBus.on("event", (event) => {
|
||||
Rpc.emit("global.event", event)
|
||||
@@ -89,8 +68,6 @@ export const rpc = {
|
||||
)
|
||||
},
|
||||
async shutdown() {
|
||||
Log.Default.info("worker shutting down")
|
||||
|
||||
await InstanceRuntime.disposeAllInstances()
|
||||
if (server) await server.stop(true)
|
||||
},
|
||||
|
||||
@@ -2,9 +2,6 @@ import path from "path"
|
||||
import { writeHeapSnapshot } from "node:v8"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
const log = Log.create({ service: "heap" })
|
||||
const MINUTE = 60_000
|
||||
const LIMIT = 2 * 1024 * 1024 * 1024
|
||||
|
||||
@@ -32,20 +29,9 @@ export function start() {
|
||||
Global.Path.log,
|
||||
`heap-${process.pid}-${new Date().toISOString().replace(/[:.]/g, "")}.heapsnapshot`,
|
||||
)
|
||||
log.warn("heap usage exceeded limit", {
|
||||
rss: stat.rss,
|
||||
heap: stat.heapUsed,
|
||||
file,
|
||||
})
|
||||
|
||||
await Promise.resolve()
|
||||
.then(() => writeHeapSnapshot(file))
|
||||
.catch((err) => {
|
||||
log.error("failed to write heap snapshot", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
file,
|
||||
})
|
||||
})
|
||||
.catch((err) => {})
|
||||
|
||||
lock = false
|
||||
}
|
||||
|
||||
@@ -3,16 +3,12 @@ export * as ConfigAgent from "./agent"
|
||||
import path from "path"
|
||||
import { Exit, Schema, SchemaGetter } from "effect"
|
||||
import { PositiveInt } from "@opencode-ai/core/schema"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Glob } from "@opencode-ai/core/util/glob"
|
||||
import { configEntryNameFromPath } from "./entry-name"
|
||||
import * as ConfigMarkdown from "./markdown"
|
||||
import { ConfigModelID } from "./model-id"
|
||||
import { ConfigParse } from "./parse"
|
||||
import { ConfigPermission } from "./permission"
|
||||
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
const Color = Schema.Union([
|
||||
Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
|
||||
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
||||
@@ -112,7 +108,6 @@ export async function load(dir: string) {
|
||||
symlink: true,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(item).catch((err) => {
|
||||
log.error("failed to load agent", { agent: item, err })
|
||||
return undefined
|
||||
})
|
||||
if (!md) continue
|
||||
@@ -138,7 +133,6 @@ export async function loadMode(dir: string) {
|
||||
symlink: true,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(item).catch((err) => {
|
||||
log.error("failed to load mode", { mode: item, err })
|
||||
return undefined
|
||||
})
|
||||
if (!md) continue
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
export * as ConfigCommand from "./command"
|
||||
|
||||
import path from "path"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Cause, Exit, Schema } from "effect"
|
||||
import { Glob } from "@opencode-ai/core/util/glob"
|
||||
import { configEntryNameFromPath } from "./entry-name"
|
||||
import { InvalidError } from "./error"
|
||||
import * as ConfigMarkdown from "./markdown"
|
||||
import { ConfigModelID } from "./model-id"
|
||||
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
template: Schema.String,
|
||||
description: Schema.optional(Schema.String),
|
||||
@@ -32,7 +28,6 @@ export async function load(dir: string) {
|
||||
symlink: true,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(item).catch((err) => {
|
||||
log.error("failed to load command", { command: item, err })
|
||||
return undefined
|
||||
})
|
||||
if (!md) continue
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
@@ -43,9 +42,6 @@ import { ConfigSkills } from "./skills"
|
||||
import { ConfigVariable } from "./variable"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
// Custom merge function that concatenates array fields instead of replacing them
|
||||
// Keep remeda's deep conditional merge type out of hot config-loading paths; TS profiling showed it dominates here.
|
||||
function mergeConfig(target: Info, source: Info): Info {
|
||||
@@ -68,7 +64,6 @@ function normalizeLoadedConfig(data: unknown, source: string) {
|
||||
delete copy.theme
|
||||
delete copy.keybinds
|
||||
delete copy.tui
|
||||
log.warn("tui keys in opencode config are deprecated; move them to tui.json", { path: source })
|
||||
return copy
|
||||
}
|
||||
|
||||
@@ -434,7 +429,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const loadFile = Effect.fnUntraced(function* (filepath: string, env?: Record<string, string>) {
|
||||
log.info("loading", { path: filepath })
|
||||
yield* Effect.logInfo("loading").pipe(Effect.annotateLogs({ service: "config", ...{ path: filepath } }))
|
||||
const text = yield* readConfigFile(filepath)
|
||||
if (!text) return {} as Info
|
||||
return yield* loadConfig(text, { path: filepath }, env)
|
||||
@@ -478,7 +473,11 @@ export const layer = Layer.effect(
|
||||
const [cachedGlobal, invalidateGlobal] = yield* Effect.cachedInvalidateWithTTL(
|
||||
loadGlobal().pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.sync(() => log.error("failed to load global config, using defaults", { error: String(error) })),
|
||||
Effect.sync(() =>
|
||||
Effect.logError("failed to load global config, using defaults").pipe(
|
||||
Effect.annotateLogs({ service: "config", ...{ error: String(error) } }),
|
||||
),
|
||||
),
|
||||
),
|
||||
Effect.orElseSucceed((): Info => ({})),
|
||||
),
|
||||
@@ -554,7 +553,9 @@ export const layer = Layer.effect(
|
||||
const url = key.replace(/\/+$/, "")
|
||||
authEnv[value.key] = value.token
|
||||
const wellknownURL = `${url}/.well-known/opencode`
|
||||
log.debug("fetching remote config", { url: wellknownURL })
|
||||
yield* Effect.logDebug("fetching remote config").pipe(
|
||||
Effect.annotateLogs({ service: "config", ...{ url: wellknownURL } }),
|
||||
)
|
||||
const wellknown = yield* fetchRemoteJson(wellknownURL, undefined, WellKnownConfig)
|
||||
const remote = yield* Effect.promise(() =>
|
||||
substituteWellKnownRemoteConfig({
|
||||
@@ -566,7 +567,9 @@ export const layer = Layer.effect(
|
||||
)
|
||||
const fetchedConfig = remote
|
||||
? yield* Effect.gen(function* () {
|
||||
log.debug("fetching remote config", { url: remote.url })
|
||||
yield* Effect.logDebug("fetching remote config").pipe(
|
||||
Effect.annotateLogs({ service: "config", ...{ url: remote.url } }),
|
||||
)
|
||||
const data = yield* fetchRemoteJson(remote.url, remote.headers, Schema.Json)
|
||||
if (isRecord(data) && isRecord(data.config)) return data.config
|
||||
if (isRecord(data)) return data
|
||||
@@ -587,7 +590,9 @@ export const layer = Layer.effect(
|
||||
authEnv,
|
||||
)
|
||||
yield* merge(source, next, "global")
|
||||
log.debug("loaded remote config from well-known", { url })
|
||||
yield* Effect.logDebug("loaded remote config from well-known").pipe(
|
||||
Effect.annotateLogs({ service: "config", ...{ url } }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,7 +601,9 @@ export const layer = Layer.effect(
|
||||
|
||||
if (Flag.OPENCODE_CONFIG) {
|
||||
yield* merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG, authEnv))
|
||||
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
|
||||
yield* Effect.logDebug("loaded custom config").pipe(
|
||||
Effect.annotateLogs({ service: "config", path: Flag.OPENCODE_CONFIG }),
|
||||
)
|
||||
}
|
||||
|
||||
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
|
||||
@@ -612,7 +619,9 @@ export const layer = Layer.effect(
|
||||
const directories = yield* ConfigPaths.directories(ctx.directory, ctx.worktree)
|
||||
|
||||
if (Flag.OPENCODE_CONFIG_DIR) {
|
||||
log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
|
||||
yield* Effect.logDebug("loading config from OPENCODE_CONFIG_DIR").pipe(
|
||||
Effect.annotateLogs({ service: "config", path: Flag.OPENCODE_CONFIG_DIR }),
|
||||
)
|
||||
}
|
||||
|
||||
const deps: Fiber.Fiber<void>[] = []
|
||||
@@ -621,7 +630,7 @@ export const layer = Layer.effect(
|
||||
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
|
||||
for (const file of ["opencode.json", "opencode.jsonc"]) {
|
||||
const source = path.join(dir, file)
|
||||
log.debug(`loading config from ${source}`)
|
||||
yield* Effect.logDebug(`loading config from ${source}`).pipe(Effect.annotateLogs({ service: "config" }))
|
||||
yield* merge(source, yield* loadFile(source, authEnv))
|
||||
result.agent ??= {}
|
||||
result.mode ??= {}
|
||||
@@ -644,9 +653,9 @@ export const layer = Layer.effect(
|
||||
Effect.exit,
|
||||
Effect.tap((exit) =>
|
||||
Exit.isFailure(exit)
|
||||
? Effect.sync(() => {
|
||||
log.warn("background dependency install failed", { dir, error: String(exit.cause) })
|
||||
})
|
||||
? Effect.logWarning("background dependency install failed").pipe(
|
||||
Effect.annotateLogs({ service: "config", dir, error: String(exit.cause) }),
|
||||
)
|
||||
: Effect.void,
|
||||
),
|
||||
Effect.asVoid,
|
||||
@@ -657,7 +666,7 @@ export const layer = Layer.effect(
|
||||
result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => ConfigCommand.load(dir)))
|
||||
result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.load(dir)))
|
||||
result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.loadMode(dir)))
|
||||
// Auto-discovered plugins under `.opencode/plugin(s)` are already local files, so ConfigPlugin.load
|
||||
// Auto-discovered plugins under `.opencode/plugin(s }))` are already local files, so ConfigPlugin.load
|
||||
// returns normalized Specs and we only need to attach origin metadata here.
|
||||
const list = yield* Effect.promise(() => ConfigPlugin.load(dir))
|
||||
yield* mergePluginOrigins(dir, list)
|
||||
@@ -670,7 +679,9 @@ export const layer = Layer.effect(
|
||||
source,
|
||||
})
|
||||
yield* merge(source, next, "local")
|
||||
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
|
||||
yield* Effect.logDebug("loaded custom config from OPENCODE_CONFIG_CONTENT").pipe(
|
||||
Effect.annotateLogs({ service: "config" }),
|
||||
)
|
||||
}
|
||||
|
||||
const activeAccount = Option.getOrUndefined(
|
||||
@@ -704,9 +715,6 @@ export const layer = Layer.effect(
|
||||
}).pipe(
|
||||
Effect.withSpan("Config.loadActiveOrgConfig"),
|
||||
Effect.catch((err) => {
|
||||
log.debug("failed to fetch remote account config", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
return Effect.void
|
||||
}),
|
||||
)
|
||||
@@ -745,7 +753,9 @@ export const layer = Layer.effect(
|
||||
try {
|
||||
result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION))
|
||||
} catch (err) {
|
||||
log.warn("OPENCODE_PERMISSION contains invalid JSON, skipping", { err })
|
||||
yield* Effect.logWarning("OPENCODE_PERMISSION contains invalid JSON, skipping").pipe(
|
||||
Effect.annotateLogs({ service: "config", ...{ err } }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -766,7 +776,9 @@ export const layer = Layer.effect(
|
||||
try {
|
||||
result.username = os.userInfo().username || "user"
|
||||
} catch (err) {
|
||||
log.warn("failed to read system username, using fallback", { err })
|
||||
yield* Effect.logWarning("failed to read system username, using fallback").pipe(
|
||||
Effect.annotateLogs({ service: "config", ...{ err } }),
|
||||
)
|
||||
result.username = "user"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,7 @@ export * as ConfigManaged from "./managed"
|
||||
import { existsSync } from "fs"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Process } from "@/util/process"
|
||||
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
const MANAGED_PLIST_DOMAIN = "ai.opencode.managed"
|
||||
|
||||
// Keys injected by macOS/MDM into the managed plist that are not OpenCode config
|
||||
@@ -50,7 +46,6 @@ export async function readManagedPreferences() {
|
||||
try {
|
||||
return os.userInfo().username || "user"
|
||||
} catch (err) {
|
||||
log.warn("failed to read system username, using fallback", { err })
|
||||
return "user"
|
||||
}
|
||||
})()
|
||||
@@ -61,10 +56,8 @@ export async function readManagedPreferences() {
|
||||
|
||||
for (const plist of paths) {
|
||||
if (!existsSync(plist)) continue
|
||||
log.info("reading macOS managed preferences", { path: plist })
|
||||
const result = await Process.run(["plutil", "-convert", "json", "-o", "-", plist], { nothrow: true })
|
||||
if (result.code !== 0) {
|
||||
log.warn("failed to convert managed preferences plist", { path: plist })
|
||||
continue
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Auth } from "@/auth"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { EventSequenceTable, EventTable } from "@/sync/event.sql"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { Slug } from "@opencode-ai/core/util/slug"
|
||||
@@ -76,9 +75,6 @@ function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
|
||||
|
||||
const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) =>
|
||||
Effect.sync(() => Database.use(fn))
|
||||
|
||||
const log = Log.create({ service: "workspace-sync" })
|
||||
|
||||
export const CreateInput = Schema.Struct({
|
||||
id: Schema.optional(WorkspaceID),
|
||||
type: Info.fields.type,
|
||||
@@ -292,24 +288,22 @@ export const layer = Layer.effect(
|
||||
return yield* store.provide({ directory: target.directory }, input.local())
|
||||
}
|
||||
|
||||
const response = yield* http.execute(input.remote({ workspace, target })).pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
log.warn("workspace target request failed", {
|
||||
workspaceID: workspace.id,
|
||||
error: errorData(error),
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
const response = yield* http
|
||||
.execute(input.remote({ workspace, target }))
|
||||
.pipe(Effect.catch((error) => Effect.sync(() => {})))
|
||||
if (!response) return input.fallback
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
const body = yield* response.text.pipe(Effect.catch(() => Effect.succeed("")))
|
||||
log.warn("workspace target request failed", {
|
||||
workspaceID: workspace.id,
|
||||
status: response.status,
|
||||
body,
|
||||
})
|
||||
yield* Effect.logWarning("workspace target request failed").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "workspace-sync",
|
||||
...{
|
||||
workspaceID: workspace.id,
|
||||
status: response.status,
|
||||
body,
|
||||
},
|
||||
}),
|
||||
)
|
||||
return input.fallback
|
||||
}
|
||||
|
||||
@@ -318,10 +312,6 @@ export const layer = Layer.effect(
|
||||
Effect.map((result) => result as A),
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
log.warn("workspace target response decode failed", {
|
||||
workspaceID: workspace.id,
|
||||
error: errorData(error),
|
||||
})
|
||||
return input.fallback
|
||||
}),
|
||||
),
|
||||
@@ -349,11 +339,16 @@ export const layer = Layer.effect(
|
||||
)
|
||||
: {}
|
||||
|
||||
log.info("syncing workspace history", {
|
||||
workspaceID: space.id,
|
||||
sessions: sessionIDs.length,
|
||||
known: Object.keys(state).length,
|
||||
})
|
||||
yield* Effect.logInfo("syncing workspace history").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "workspace-sync",
|
||||
...{
|
||||
workspaceID: space.id,
|
||||
sessions: sessionIDs.length,
|
||||
known: Object.keys(state).length,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const response = yield* http.execute(
|
||||
HttpClientRequest.post(route(url, "/sync/history"), {
|
||||
@@ -373,10 +368,15 @@ export const layer = Layer.effect(
|
||||
|
||||
const events = (yield* response.json) as HistoryEvent[]
|
||||
|
||||
log.info("workspace history synced", {
|
||||
workspaceID: space.id,
|
||||
events: events.length,
|
||||
})
|
||||
yield* Effect.logInfo("workspace history synced").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "workspace-sync",
|
||||
...{
|
||||
workspaceID: space.id,
|
||||
events: events.length,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
yield* Effect.forEach(
|
||||
events,
|
||||
@@ -405,7 +405,9 @@ export const layer = Layer.effect(
|
||||
let attempt = 0
|
||||
|
||||
while (true) {
|
||||
log.info("connecting to global sync", { workspace: space.name })
|
||||
yield* Effect.logInfo("connecting to global sync").pipe(
|
||||
Effect.annotateLogs({ service: "workspace-sync", ...{ workspace: space.name } }),
|
||||
)
|
||||
setStatus(space.id, "connecting")
|
||||
|
||||
const stream = yield* connectSSE(target.url, target.headers).pipe(
|
||||
@@ -413,10 +415,6 @@ export const layer = Layer.effect(
|
||||
Effect.catch((err) =>
|
||||
Effect.sync(() => {
|
||||
setStatus(space.id, "error")
|
||||
log.info("failed to connect to global sync", {
|
||||
workspace: space.name,
|
||||
err,
|
||||
})
|
||||
return null
|
||||
}),
|
||||
),
|
||||
@@ -425,7 +423,9 @@ export const layer = Layer.effect(
|
||||
if (stream) {
|
||||
attempt = 0
|
||||
|
||||
log.info("global sync connected", { workspace: space.name })
|
||||
yield* Effect.logInfo("global sync connected").pipe(
|
||||
Effect.annotateLogs({ service: "workspace-sync", ...{ workspace: space.name } }),
|
||||
)
|
||||
setStatus(space.id, "connected")
|
||||
|
||||
yield* parseSSE(stream, (evt) =>
|
||||
@@ -439,10 +439,6 @@ export const layer = Layer.effect(
|
||||
Effect.as(false),
|
||||
Effect.catchCause((error) =>
|
||||
Effect.sync(() => {
|
||||
log.info("failed to replay global event", {
|
||||
workspaceID: space.id,
|
||||
error,
|
||||
})
|
||||
return true
|
||||
}),
|
||||
),
|
||||
@@ -459,15 +455,22 @@ export const layer = Layer.effect(
|
||||
payload: event.payload,
|
||||
})
|
||||
} catch (error) {
|
||||
log.info("failed to replay global event", {
|
||||
workspaceID: space.id,
|
||||
error,
|
||||
})
|
||||
yield* Effect.logInfo("failed to replay global event").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "workspace-sync",
|
||||
...{
|
||||
workspaceID: space.id,
|
||||
error,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
log.info("disconnected from global sync: " + space.id)
|
||||
yield* Effect.logInfo("disconnected from global sync: " + space.id).pipe(
|
||||
Effect.annotateLogs({ service: "workspace-sync" }),
|
||||
)
|
||||
setStatus(space.id, "disconnected")
|
||||
}
|
||||
|
||||
@@ -485,10 +488,6 @@ export const layer = Layer.effect(
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
setStatus(space.id, "error")
|
||||
log.warn("workspace target failed", {
|
||||
workspaceID: space.id,
|
||||
error: errorData(error),
|
||||
})
|
||||
return null
|
||||
}),
|
||||
),
|
||||
@@ -514,10 +513,6 @@ export const layer = Layer.effect(
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
setStatus(space.id, "error")
|
||||
log.warn("workspace listener failed", {
|
||||
workspaceID: space.id,
|
||||
error,
|
||||
})
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -598,10 +593,15 @@ export const layer = Layer.effect(
|
||||
|
||||
const sessionWarp = Effect.fn("Workspace.sessionWarp")(function* (input: SessionWarpInput) {
|
||||
return yield* Effect.gen(function* () {
|
||||
log.info("session warp requested", {
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
})
|
||||
yield* Effect.logInfo("session warp requested").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "workspace-sync",
|
||||
...{
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const current = yield* db((db) =>
|
||||
db
|
||||
@@ -618,15 +618,7 @@ export const layer = Layer.effect(
|
||||
|
||||
if (target.type === "remote") {
|
||||
yield* syncHistory(previous, target.url, target.headers).pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
log.warn("session warp final source sync failed", {
|
||||
workspaceID: previous.id,
|
||||
sessionID: input.sessionID,
|
||||
error: errorData(error),
|
||||
})
|
||||
}),
|
||||
),
|
||||
Effect.catch((error) => Effect.sync(() => {})),
|
||||
)
|
||||
} else {
|
||||
yield* prompt.cancel(input.sessionID)
|
||||
@@ -676,11 +668,16 @@ export const layer = Layer.effect(
|
||||
},
|
||||
})
|
||||
|
||||
log.info("session warp complete", {
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
target: "local",
|
||||
})
|
||||
yield* Effect.logInfo("session warp complete").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "workspace-sync",
|
||||
...{
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
target: "local",
|
||||
},
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -702,11 +699,16 @@ export const layer = Layer.effect(
|
||||
},
|
||||
})
|
||||
|
||||
log.info("session warp complete", {
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
target: target.directory,
|
||||
})
|
||||
yield* Effect.logInfo("session warp complete").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "workspace-sync",
|
||||
...{
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
target: target.directory,
|
||||
},
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -733,15 +735,20 @@ export const layer = Layer.effect(
|
||||
const batches = Iterable.chunksOf(rows, 10)
|
||||
const total = Iterable.size(batches)
|
||||
|
||||
log.info("session warp prepared", {
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
target: String(route(target.url, "/sync/replay")),
|
||||
events: rows.length,
|
||||
batches: total,
|
||||
first: rows[0]?.seq,
|
||||
last: rows.at(-1)?.seq,
|
||||
})
|
||||
yield* Effect.logInfo("session warp prepared").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "workspace-sync",
|
||||
...{
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
target: String(route(target.url, "/sync/replay")),
|
||||
events: rows.length,
|
||||
batches: total,
|
||||
first: rows[0]?.seq,
|
||||
last: rows.at(-1)?.seq,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
yield* Effect.forEach(
|
||||
batches,
|
||||
@@ -759,14 +766,19 @@ export const layer = Layer.effect(
|
||||
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
const body = yield* response.text
|
||||
log.error("session warp batch failed", {
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
step: i + 1,
|
||||
total,
|
||||
status: response.status,
|
||||
body,
|
||||
})
|
||||
yield* Effect.logError("session warp batch failed").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "workspace-sync",
|
||||
...{
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
step: i + 1,
|
||||
total,
|
||||
status: response.status,
|
||||
body,
|
||||
},
|
||||
}),
|
||||
)
|
||||
return yield* new SessionWarpHttpError({
|
||||
message: `Failed to warp session ${input.sessionID} into workspace ${workspaceID}: HTTP ${response.status} ${body}`,
|
||||
workspaceID,
|
||||
@@ -776,13 +788,18 @@ export const layer = Layer.effect(
|
||||
})
|
||||
}
|
||||
|
||||
log.info("session warp batch posted", {
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
step: i + 1,
|
||||
total,
|
||||
status: response.status,
|
||||
})
|
||||
yield* Effect.logInfo("session warp batch posted").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "workspace-sync",
|
||||
...{
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
step: i + 1,
|
||||
total,
|
||||
status: response.status,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}),
|
||||
{ discard: true },
|
||||
)
|
||||
@@ -795,12 +812,17 @@ export const layer = Layer.effect(
|
||||
)
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
const body = yield* response.text
|
||||
log.error("session warp steal failed", {
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
status: response.status,
|
||||
body,
|
||||
})
|
||||
yield* Effect.logError("session warp steal failed").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "workspace-sync",
|
||||
...{
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
status: response.status,
|
||||
body,
|
||||
},
|
||||
}),
|
||||
)
|
||||
return yield* new SessionWarpHttpError({
|
||||
message: `Failed to steal session ${input.sessionID} into workspace ${workspaceID}: HTTP ${response.status} ${body}`,
|
||||
workspaceID,
|
||||
@@ -810,19 +832,29 @@ export const layer = Layer.effect(
|
||||
})
|
||||
}
|
||||
|
||||
log.info("session warp complete", {
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
batches: total,
|
||||
})
|
||||
yield* Effect.logInfo("session warp complete").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "workspace-sync",
|
||||
...{
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
batches: total,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}).pipe(
|
||||
Effect.tapError((err) =>
|
||||
Effect.sync(() =>
|
||||
log.error("session warp failed", {
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
error: errorData(err),
|
||||
}),
|
||||
Effect.logError("session warp failed").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "workspace-sync",
|
||||
...{
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
error: errorData(err),
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -848,7 +880,6 @@ export const layer = Layer.effect(
|
||||
WorkspaceAdapterRuntime.list(adapter).pipe(
|
||||
Effect.catchCause((error) =>
|
||||
Effect.sync(() => {
|
||||
log.warn("workspace adapter list failed", { type, error })
|
||||
return []
|
||||
}),
|
||||
),
|
||||
@@ -927,10 +958,7 @@ export const layer = Layer.effect(
|
||||
Effect.gen(function* () {
|
||||
yield* WorkspaceAdapterRuntime.remove(info)
|
||||
}),
|
||||
() =>
|
||||
Effect.sync(() => {
|
||||
log.error("adapter not available when removing workspace", { type: row.type })
|
||||
}),
|
||||
() => Effect.sync(() => {}),
|
||||
)
|
||||
|
||||
yield* db((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
|
||||
@@ -996,10 +1024,6 @@ export const layer = Layer.effect(
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
setStatus(workspace.id, "error")
|
||||
log.warn("workspace sync failed to start", {
|
||||
workspaceID: workspace.id,
|
||||
error,
|
||||
})
|
||||
}),
|
||||
),
|
||||
Effect.forkDetach,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { Database } from "./storage/db"
|
||||
import { DataMigrationTable } from "./data-migration.sql"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { and, asc, eq, gt, inArray, sql } from "drizzle-orm"
|
||||
import { MessageTable, SessionTable } from "./session/session.sql"
|
||||
import type { SessionID } from "./session/schema"
|
||||
@@ -10,9 +9,6 @@ export type Migration<R = never> = {
|
||||
name: string
|
||||
run: Effect.Effect<void, unknown, R>
|
||||
}
|
||||
|
||||
const log = Log.create({ service: "data-migration" })
|
||||
|
||||
export interface Interface {}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/DataMigration") {}
|
||||
@@ -135,7 +131,9 @@ export const layer = Layer.effect(
|
||||
)
|
||||
if (completed) continue
|
||||
|
||||
log.info("running data migration", { name: migration.name })
|
||||
yield* Effect.logInfo("running data migration").pipe(
|
||||
Effect.annotateLogs({ service: "data-migration", ...{ name: migration.name } }),
|
||||
)
|
||||
yield* migration.run.pipe(Effect.withSpan("DataMigration", { attributes: { name: migration.name } }))
|
||||
Database.use((db) =>
|
||||
db
|
||||
|
||||
@@ -12,7 +12,6 @@ import ignore from "ignore"
|
||||
import path from "path"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { containsPath } from "../project/instance-context"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Protected } from "./protected"
|
||||
import { Ripgrep } from "./ripgrep"
|
||||
import { NonNegativeInt, type DeepMutable } from "@opencode-ai/core/schema"
|
||||
@@ -69,9 +68,6 @@ export const Event = {
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
const log = Log.create({ service: "file" })
|
||||
|
||||
const binary = new Set([
|
||||
"exe",
|
||||
"dll",
|
||||
@@ -284,7 +280,6 @@ const getImageMimeType = (file: string) => mime[ext(file)] || "image/" + ext(fil
|
||||
|
||||
function shouldEncode(mimeType: string) {
|
||||
const type = mimeType.toLowerCase()
|
||||
log.debug("shouldEncode", { type })
|
||||
if (!type) return false
|
||||
if (type.startsWith("text/")) return false
|
||||
if (type.includes("charset=")) return false
|
||||
@@ -499,7 +494,6 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const read: Interface["read"] = Effect.fn("File.read")(function* (file: string) {
|
||||
using _ = log.time("read", { file })
|
||||
const ctx = yield* InstanceState.context
|
||||
const full = path.join(ctx.directory, file)
|
||||
|
||||
@@ -621,7 +615,7 @@ export const layer = Layer.effect(
|
||||
const query = input.query.trim()
|
||||
const limit = input.limit ?? 100
|
||||
const kind = input.type ?? (input.dirs === false ? "file" : "all")
|
||||
log.info("search", { query, kind })
|
||||
yield* Effect.logInfo("search").pipe(Effect.annotateLogs({ service: "file", ...{ query, kind } }))
|
||||
|
||||
const preferHidden = query.startsWith(".") || query.includes("/.")
|
||||
|
||||
@@ -636,11 +630,13 @@ export const layer = Layer.effect(
|
||||
const sorted = fuzzysort.go(query, items, { limit: searchLimit }).map((item) => item.target)
|
||||
const output = kind === "directory" ? sortHiddenLast(sorted, preferHidden).slice(0, limit) : sorted
|
||||
|
||||
log.info("search", { query, kind, results: output.length })
|
||||
yield* Effect.logInfo("search").pipe(
|
||||
Effect.annotateLogs({ service: "file", ...{ query, kind, results: output.length } }),
|
||||
)
|
||||
return output
|
||||
})
|
||||
|
||||
log.info("init")
|
||||
yield* Effect.logInfo("init").pipe(Effect.annotateLogs({ service: "file" }))
|
||||
return Service.of({ init, status, read, list, search })
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -9,12 +9,9 @@ import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner
|
||||
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { sanitizedProcessEnv } from "@opencode-ai/core/util/opencode-process"
|
||||
import { which } from "@/util/which"
|
||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||
|
||||
const log = Log.create({ service: "ripgrep" })
|
||||
const VERSION = "15.1.0"
|
||||
const PLATFORM = {
|
||||
"arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" },
|
||||
@@ -306,7 +303,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | ChildPro
|
||||
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}`
|
||||
const archive = path.join(Global.Path.bin, filename)
|
||||
|
||||
log.info("downloading ripgrep", { url })
|
||||
yield* Effect.logInfo("downloading ripgrep").pipe(Effect.annotateLogs({ service: "ripgrep", ...{ url } }))
|
||||
yield* fs.ensureDir(Global.Path.bin).pipe(Effect.orDie)
|
||||
|
||||
const bytes = yield* HttpClientRequest.get(url).pipe(
|
||||
@@ -417,7 +414,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | ChildPro
|
||||
})
|
||||
|
||||
const tree: Interface["tree"] = Effect.fn("Ripgrep.tree")(function* (input: TreeInput) {
|
||||
log.info("tree", input)
|
||||
yield* Effect.logInfo("tree").pipe(Effect.annotateLogs({ service: "ripgrep", ...input }))
|
||||
const list = Array.from(yield* files({ cwd: input.cwd, signal: input.signal }).pipe(Stream.runCollect))
|
||||
|
||||
interface Node {
|
||||
|
||||
@@ -14,11 +14,8 @@ import { lazy } from "@/util/lazy"
|
||||
import { Config } from "@/config/config"
|
||||
import { FileIgnore } from "./ignore"
|
||||
import { Protected } from "./protected"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
declare const OPENCODE_LIBC: string | undefined
|
||||
|
||||
const log = Log.create({ service: "file.watcher" })
|
||||
const SUBSCRIBE_TIMEOUT_MS = 10_000
|
||||
|
||||
export const Event = {
|
||||
@@ -38,7 +35,6 @@ const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
|
||||
)
|
||||
return createWrapper(binding) as typeof import("@parcel/watcher")
|
||||
} catch (error) {
|
||||
log.error("failed to load watcher binding", { error })
|
||||
return
|
||||
}
|
||||
})
|
||||
@@ -77,18 +73,30 @@ export const layer = Layer.effect(
|
||||
|
||||
const ctx = yield* InstanceState.context
|
||||
|
||||
log.info("init", { directory: ctx.directory })
|
||||
yield* Effect.logInfo("init").pipe(
|
||||
Effect.annotateLogs({ service: "file.watcher", ...{ directory: ctx.directory } }),
|
||||
)
|
||||
|
||||
const backend = getBackend()
|
||||
if (!backend) {
|
||||
log.error("watcher backend not supported", { directory: ctx.directory, platform: process.platform })
|
||||
yield* Effect.logError("watcher backend not supported").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "file.watcher",
|
||||
...{ directory: ctx.directory, platform: process.platform },
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const w = watcher()
|
||||
if (!w) return
|
||||
|
||||
log.info("watcher backend", { directory: ctx.directory, platform: process.platform, backend })
|
||||
yield* Effect.logInfo("watcher backend").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "file.watcher",
|
||||
...{ directory: ctx.directory, platform: process.platform, backend },
|
||||
}),
|
||||
)
|
||||
const bridge = yield* EffectBridge.make()
|
||||
const subs: ParcelWatcher.AsyncSubscription[] = []
|
||||
yield* Effect.addFinalizer(() =>
|
||||
@@ -112,7 +120,6 @@ export const layer = Layer.effect(
|
||||
}).pipe(
|
||||
Effect.timeout(SUBSCRIBE_TIMEOUT_MS),
|
||||
Effect.catchCause((cause) => {
|
||||
log.error("failed to subscribe", { dir, cause: Cause.pretty(cause) })
|
||||
pending.then((s) => s.unsubscribe()).catch(() => {})
|
||||
return Effect.void
|
||||
}),
|
||||
@@ -148,7 +155,6 @@ export const layer = Layer.effect(
|
||||
}
|
||||
},
|
||||
Effect.catchCause((cause) => {
|
||||
log.error("failed to init watcher service", { cause: Cause.pretty(cause) })
|
||||
return Effect.void
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -8,11 +8,7 @@ import { mergeDeep } from "remeda"
|
||||
import { Config } from "@/config/config"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { errorMessage } from "@/util/error"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import * as Formatter from "./formatter"
|
||||
|
||||
const log = Log.create({ service: "format" })
|
||||
|
||||
export const Status = Schema.Struct({
|
||||
name: Schema.String,
|
||||
extensions: Schema.Array(Schema.String),
|
||||
@@ -60,10 +56,8 @@ export const layer = Layer.effect(
|
||||
const matching = Object.values(formatters).filter((item) => item.extensions.includes(ext))
|
||||
const checks = await Promise.all(
|
||||
matching.map(async (item) => {
|
||||
log.info("checking", { name: item.name, ext })
|
||||
const cmd = await getCommand(item)
|
||||
if (cmd) {
|
||||
log.info("enabled", { name: item.name, ext })
|
||||
}
|
||||
return {
|
||||
item,
|
||||
@@ -78,13 +72,13 @@ export const layer = Layer.effect(
|
||||
|
||||
function formatFile(filepath: string) {
|
||||
return Effect.gen(function* () {
|
||||
log.info("formatting", { file: filepath })
|
||||
yield* Effect.logInfo("formatting").pipe(Effect.annotateLogs({ service: "format", ...{ file: filepath } }))
|
||||
const formatters = yield* Effect.promise(() => getFormatter(path.extname(filepath)))
|
||||
|
||||
if (!formatters.length) return false
|
||||
|
||||
for (const { item, cmd } of formatters) {
|
||||
log.info("running", { command: cmd })
|
||||
yield* Effect.logInfo("running").pipe(Effect.annotateLogs({ service: "format", ...{ command: cmd } }))
|
||||
const replaced = cmd.map((x) => x.replace("$FILE", filepath))
|
||||
const dir = yield* InstanceState.directory
|
||||
const result = yield* appProcess
|
||||
@@ -101,22 +95,20 @@ export const layer = Layer.effect(
|
||||
.pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
log.error("failed to format file", {
|
||||
error: "spawn failed",
|
||||
command: cmd,
|
||||
...item.environment,
|
||||
file: filepath,
|
||||
cause: errorMessage(error.cause ?? error),
|
||||
})
|
||||
return undefined
|
||||
}),
|
||||
),
|
||||
)
|
||||
if (result && result.exitCode !== 0) {
|
||||
log.error("failed", {
|
||||
command: cmd,
|
||||
...item.environment,
|
||||
})
|
||||
yield* Effect.logError("failed").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "format",
|
||||
...{
|
||||
command: cmd,
|
||||
...item.environment,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,8 +119,8 @@ export const layer = Layer.effect(
|
||||
const cfg = yield* config.get()
|
||||
|
||||
if (!cfg.formatter) {
|
||||
log.info("all formatters are disabled")
|
||||
log.info("init")
|
||||
yield* Effect.logInfo("all formatters are disabled").pipe(Effect.annotateLogs({ service: "format" }))
|
||||
yield* Effect.logInfo("init").pipe(Effect.annotateLogs({ service: "format" }))
|
||||
return {
|
||||
formatters,
|
||||
isEnabled,
|
||||
@@ -166,7 +158,7 @@ export const layer = Layer.effect(
|
||||
}
|
||||
}
|
||||
|
||||
log.info("init")
|
||||
yield* Effect.logInfo("init").pipe(Effect.annotateLogs({ service: "format" }))
|
||||
|
||||
return {
|
||||
formatters,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Schema } from "effect"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Process } from "@/util/process"
|
||||
|
||||
const SUPPORTED_IDES = [
|
||||
@@ -11,9 +10,6 @@ const SUPPORTED_IDES = [
|
||||
{ name: "Cursor" as const, cmd: "cursor" },
|
||||
{ name: "VSCodium" as const, cmd: "codium" },
|
||||
]
|
||||
|
||||
const log = Log.create({ service: "ide" })
|
||||
|
||||
export const Event = {
|
||||
Installed: BusEvent.define(
|
||||
"ide.installed",
|
||||
@@ -52,13 +48,6 @@ export async function install(ide: (typeof SUPPORTED_IDES)[number]["name"]) {
|
||||
})
|
||||
const stdout = p.stdout.toString()
|
||||
const stderr = p.stderr.toString()
|
||||
|
||||
log.info("installed", {
|
||||
ide,
|
||||
stdout,
|
||||
stderr,
|
||||
})
|
||||
|
||||
if (p.code !== 0) {
|
||||
throw new InstallFailedError({ stderr })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Config } from "@/config/config"
|
||||
import type { MessageV2 } from "@/session/message-v2"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import photonWasm from "@silvia-odwyer/photon-node/photon_rs_bg.wasm" with { type: "file" }
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import path from "node:path"
|
||||
@@ -11,8 +10,6 @@ const MAX_WIDTH = 2000
|
||||
const MAX_HEIGHT = 2000
|
||||
const AUTO_RESIZE = true
|
||||
const JPEG_QUALITIES = [80, 85, 70, 55, 40]
|
||||
const log = Log.create({ service: "image" })
|
||||
|
||||
export class ResizerUnavailableError extends Schema.TaggedErrorClass<ResizerUnavailableError>()(
|
||||
"ImageResizerUnavailableError",
|
||||
{},
|
||||
@@ -68,7 +65,11 @@ export const layer = Layer.effect(
|
||||
path.isAbsolute(photonWasm) ? photonWasm : fileURLToPath(new URL(photonWasm, import.meta.url))
|
||||
}).pipe(
|
||||
Effect.andThen(() => Effect.tryPromise(() => import("@silvia-odwyer/photon-node"))),
|
||||
Effect.tapError((error) => Effect.sync(() => log.warn("failed to load photon", { error }))),
|
||||
Effect.tapError((error) =>
|
||||
Effect.sync(() =>
|
||||
Effect.logWarning("failed to load photon").pipe(Effect.annotateLogs({ service: "image", ...{ error } })),
|
||||
),
|
||||
),
|
||||
Effect.mapError(() => new ResizerUnavailableError()),
|
||||
),
|
||||
)
|
||||
@@ -92,7 +93,6 @@ export const layer = Layer.effect(
|
||||
const decoded = yield* Effect.try({
|
||||
try: () => photon.PhotonImage.new_from_byteslice(Buffer.from(base64, "base64")),
|
||||
catch: (error) => {
|
||||
log.warn("failed to decode image", { error })
|
||||
return new DecodeError()
|
||||
},
|
||||
})
|
||||
@@ -140,12 +140,17 @@ export const layer = Layer.effect(
|
||||
resized.free()
|
||||
|
||||
if (candidate) {
|
||||
log.info("using resized image", {
|
||||
from_mime: input.mime,
|
||||
to_mime: candidate.mime,
|
||||
from: `${originalWidth}x${originalHeight}`,
|
||||
to: `${size.width}x${size.height}`,
|
||||
})
|
||||
yield* Effect.logInfo("using resized image").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "image",
|
||||
...{
|
||||
from_mime: input.mime,
|
||||
to_mime: candidate.mime,
|
||||
from: `${originalWidth}x${originalHeight}`,
|
||||
to: `${size.width}x${size.height}`,
|
||||
},
|
||||
}),
|
||||
)
|
||||
return {
|
||||
...input,
|
||||
mime: candidate.mime,
|
||||
|
||||
@@ -2,7 +2,6 @@ import yargs from "yargs"
|
||||
import { hideBin } from "yargs/helpers"
|
||||
import { RunCommand } from "./cli/cmd/run"
|
||||
import { GenerateCommand } from "./cli/cmd/generate"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { ConsoleCommand } from "./cli/cmd/account"
|
||||
import { ProvidersCommand } from "./cli/cmd/providers"
|
||||
import { AgentCommand } from "./cli/cmd/agent"
|
||||
@@ -12,7 +11,6 @@ import { ModelsCommand } from "./cli/cmd/models"
|
||||
import { UI } from "./cli/ui"
|
||||
import { Installation } from "./installation"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { FormatError } from "./cli/error"
|
||||
import { ServeCommand } from "./cli/cmd/serve"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
@@ -38,24 +36,9 @@ import { errorMessage } from "./util/error"
|
||||
import { PluginCommand } from "./cli/cmd/plug"
|
||||
import { Heap } from "./cli/heap"
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite"
|
||||
import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process"
|
||||
import { isRecord } from "@/util/record"
|
||||
|
||||
const processMetadata = ensureProcessMetadata("main")
|
||||
|
||||
process.on("unhandledRejection", (e) => {
|
||||
Log.Default.error("rejection", {
|
||||
e: errorMessage(e),
|
||||
})
|
||||
})
|
||||
|
||||
process.on("uncaughtException", (e) => {
|
||||
Log.Default.error("exception", {
|
||||
e: errorMessage(e),
|
||||
})
|
||||
})
|
||||
|
||||
const args = hideBin(process.argv)
|
||||
type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR"
|
||||
|
||||
function show(out: string) {
|
||||
const text = out.trimStart()
|
||||
@@ -67,6 +50,11 @@ function show(out: string) {
|
||||
process.stderr.write(out)
|
||||
}
|
||||
|
||||
function parseLogLevel(value: string | undefined): LogLevel | undefined {
|
||||
if (value === "DEBUG" || value === "INFO" || value === "WARN" || value === "ERROR") return value
|
||||
return undefined
|
||||
}
|
||||
|
||||
const cli = yargs(args)
|
||||
.parserConfiguration({ "populate--": true })
|
||||
.scriptName("opencode")
|
||||
@@ -79,6 +67,10 @@ const cli = yargs(args)
|
||||
describe: "print logs to stderr",
|
||||
type: "boolean",
|
||||
})
|
||||
.option("log-file", {
|
||||
describe: "path to JSONL log file",
|
||||
type: "string",
|
||||
})
|
||||
.option("log-level", {
|
||||
describe: "log level",
|
||||
type: "string",
|
||||
@@ -93,15 +85,12 @@ const cli = yargs(args)
|
||||
process.env.OPENCODE_PURE = "1"
|
||||
}
|
||||
|
||||
await Log.init({
|
||||
print: process.argv.includes("--print-logs"),
|
||||
dev: Installation.isLocal(),
|
||||
level: (() => {
|
||||
if (opts.logLevel) return opts.logLevel as Log.Level
|
||||
if (Installation.isLocal()) return "DEBUG"
|
||||
return "INFO"
|
||||
})(),
|
||||
})
|
||||
if (opts.printLogs !== undefined) process.env.OPENCODE_PRINT_LOGS = opts.printLogs ? "1" : "0"
|
||||
if (opts.logFile) process.env.OPENCODE_LOG_FILE = opts.logFile
|
||||
const envLevel = parseLogLevel(process.env.OPENCODE_LOG_LEVEL)
|
||||
process.env.OPENCODE_LOG_LEVEL = opts.logLevel
|
||||
? (opts.logLevel as LogLevel)
|
||||
: (envLevel ?? (Installation.isLocal() ? "DEBUG" : "INFO"))
|
||||
|
||||
Heap.start()
|
||||
|
||||
@@ -109,13 +98,6 @@ const cli = yargs(args)
|
||||
process.env.OPENCODE = "1"
|
||||
process.env.OPENCODE_PID = String(process.pid)
|
||||
|
||||
Log.Default.info("opencode", {
|
||||
version: InstallationVersion,
|
||||
args: process.argv.slice(2),
|
||||
process_role: processMetadata.processRole,
|
||||
run_id: processMetadata.runID,
|
||||
})
|
||||
|
||||
const marker = path.join(Global.Path.data, "opencode.db")
|
||||
if (!(await Filesystem.exists(marker))) {
|
||||
const tty = process.stderr.isTTY
|
||||
@@ -203,42 +185,10 @@ try {
|
||||
await cli.parse()
|
||||
}
|
||||
} catch (e) {
|
||||
let data: Record<string, any> = {}
|
||||
if (e instanceof Error) {
|
||||
Object.assign(data, {
|
||||
name: e.name,
|
||||
message: e.message,
|
||||
cause: e.cause?.toString(),
|
||||
stack: e.stack,
|
||||
})
|
||||
}
|
||||
|
||||
if (e instanceof NamedError) {
|
||||
const obj = e.toObject()
|
||||
if (isRecord(obj.data)) {
|
||||
for (const [key, value] of Object.entries(obj.data)) {
|
||||
if (key === "name" || key === "stack" || key === "cause") continue
|
||||
data[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e instanceof ResolveMessage) {
|
||||
Object.assign(data, {
|
||||
name: e.name,
|
||||
message: e.message,
|
||||
code: e.code,
|
||||
specifier: e.specifier,
|
||||
referrer: e.referrer,
|
||||
position: e.position,
|
||||
importKind: e.importKind,
|
||||
})
|
||||
}
|
||||
Log.Default.error("fatal", data)
|
||||
const formatted = FormatError(e)
|
||||
if (formatted) UI.error(formatted)
|
||||
if (formatted === undefined) {
|
||||
UI.error("Unexpected error, check log file at " + Log.file() + " for more details" + EOL)
|
||||
UI.error("Unexpected error" + EOL)
|
||||
process.stderr.write(errorMessage(e) + EOL)
|
||||
}
|
||||
process.exitCode = 1
|
||||
|
||||
@@ -7,14 +7,10 @@ import { ChildProcess } from "effect/unstable/process"
|
||||
import { AppProcess } from "@opencode-ai/core/process"
|
||||
import path from "path"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { makeRuntime } from "@opencode-ai/core/effect/runtime"
|
||||
import semver from "semver"
|
||||
import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { NpmConfig } from "@opencode-ai/core/npm-config"
|
||||
|
||||
const log = Log.create({ service: "installation" })
|
||||
|
||||
export type Method = "curl" | "npm" | "yarn" | "pnpm" | "bun" | "brew" | "scoop" | "choco" | "unknown"
|
||||
|
||||
export type ReleaseType = "patch" | "minor" | "major"
|
||||
@@ -317,12 +313,17 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProce
|
||||
if (!upgradeResult || upgradeResult.code !== 0) {
|
||||
return yield* new UpgradeFailedError({ stderr: upgradeFailure(m, upgradeResult) })
|
||||
}
|
||||
log.info("upgraded", {
|
||||
method: m,
|
||||
target,
|
||||
stdout: upgradeResult.stdout,
|
||||
stderr: upgradeResult.stderr,
|
||||
})
|
||||
yield* Effect.logInfo("upgraded").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "installation",
|
||||
...{
|
||||
method: m,
|
||||
target,
|
||||
stdout: upgradeResult.stdout,
|
||||
stderr: upgradeResult.stderr,
|
||||
},
|
||||
}),
|
||||
)
|
||||
yield* text([process.execPath, "--version"])
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import path from "path"
|
||||
import { pathToFileURL, fileURLToPath } from "url"
|
||||
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
|
||||
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Process } from "@/util/process"
|
||||
import { LANGUAGE_EXTENSIONS } from "./language"
|
||||
import { Effect, Schema } from "effect"
|
||||
@@ -26,8 +25,6 @@ const INITIALIZE_TIMEOUT_MS = 45_000
|
||||
const FILE_CHANGE_CREATED = 1
|
||||
const FILE_CHANGE_CHANGED = 2
|
||||
const TEXT_DOCUMENT_SYNC_INCREMENTAL = 2
|
||||
|
||||
const log = Log.create({ service: "lsp.client" })
|
||||
const busRuntime = makeRuntime(Bus.Service, Bus.layer)
|
||||
|
||||
export type Info = NonNullable<Awaited<ReturnType<typeof create>>>
|
||||
@@ -145,23 +142,13 @@ export async function create(input: {
|
||||
directory: string
|
||||
instance: InstanceContext
|
||||
}) {
|
||||
const logger = log.clone().tag("serverID", input.serverID)
|
||||
logger.info("starting client")
|
||||
const logAnnotations = { service: "lsp.client", serverID: input.serverID }
|
||||
const instance = input.instance
|
||||
|
||||
const connection = createMessageConnection(
|
||||
new StreamMessageReader(input.server.process.stdout as any),
|
||||
new StreamMessageWriter(input.server.process.stdin as any),
|
||||
)
|
||||
// Server stderr can contain both real errors and routine informational logs,
|
||||
// which is normal stderr practice for some tools. Keep the raw stream at
|
||||
// debug so users can opt in with --print-logs --log-level DEBUG without
|
||||
// polluting normal logs.
|
||||
input.server.process.stderr?.on("data", (data: Buffer) => {
|
||||
const text = data.toString().trim()
|
||||
if (text) logger.debug("server stderr", { text: text.slice(0, 1000) })
|
||||
})
|
||||
|
||||
// --- Connection state ---
|
||||
|
||||
const pushDiagnostics = new Map<string, Diagnostic[]>()
|
||||
@@ -191,11 +178,6 @@ export async function create(input: {
|
||||
connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
||||
const filePath = getFilePath(params.uri)
|
||||
if (!filePath) return
|
||||
logger.info("textDocument/publishDiagnostics", {
|
||||
path: filePath,
|
||||
count: params.diagnostics.length,
|
||||
version: params.version,
|
||||
})
|
||||
published.set(filePath, {
|
||||
at: Date.now(),
|
||||
version: typeof params.version === "number" ? params.version : undefined,
|
||||
@@ -207,7 +189,6 @@ export async function create(input: {
|
||||
updatePushDiagnostics(filePath, params.diagnostics)
|
||||
})
|
||||
connection.onRequest("window/workDoneProgress/create", (params) => {
|
||||
logger.info("window/workDoneProgress/create", params)
|
||||
return null
|
||||
})
|
||||
connection.onRequest("workspace/configuration", async (params) => {
|
||||
@@ -242,10 +223,6 @@ export async function create(input: {
|
||||
])
|
||||
connection.onRequest("workspace/diagnostic/refresh", async () => null)
|
||||
connection.listen()
|
||||
|
||||
// --- Initialize handshake ---
|
||||
|
||||
logger.info("sending initialize")
|
||||
const initialized = await withTimeout(
|
||||
connection.sendRequest<{ capabilities?: ServerCapabilities }>("initialize", {
|
||||
rootUri: pathToFileURL(input.root).href,
|
||||
@@ -289,7 +266,6 @@ export async function create(input: {
|
||||
}),
|
||||
INITIALIZE_TIMEOUT_MS,
|
||||
).catch((err) => {
|
||||
logger.error("initialize error", { error: err })
|
||||
throw new InitializeError({ serverID: input.serverID, cause: err })
|
||||
})
|
||||
|
||||
@@ -602,11 +578,6 @@ export async function create(input: {
|
||||
|
||||
const document = files[request.path]
|
||||
if (document !== undefined) {
|
||||
// Do not wipe diagnostics on didChange. Some servers (e.g. clangd) only
|
||||
// re-emit diagnostics when the content actually changes, so clearing
|
||||
// here would lose errors for no-op touchFile calls. Let the server's
|
||||
// next push/pull overwrite naturally.
|
||||
logger.info("workspace/didChangeWatchedFiles", request)
|
||||
await connection.sendNotification("workspace/didChangeWatchedFiles", {
|
||||
changes: [
|
||||
{
|
||||
@@ -618,10 +589,6 @@ export async function create(input: {
|
||||
|
||||
const next = document.version + 1
|
||||
files[request.path] = { version: next, text }
|
||||
logger.info("textDocument/didChange", {
|
||||
path: request.path,
|
||||
version: next,
|
||||
})
|
||||
await connection.sendNotification("textDocument/didChange", {
|
||||
textDocument: {
|
||||
uri: pathToFileURL(request.path).href,
|
||||
@@ -642,8 +609,6 @@ export async function create(input: {
|
||||
})
|
||||
return next
|
||||
}
|
||||
|
||||
logger.info("workspace/didChangeWatchedFiles", request)
|
||||
await connection.sendNotification("workspace/didChangeWatchedFiles", {
|
||||
changes: [
|
||||
{
|
||||
@@ -652,8 +617,6 @@ export async function create(input: {
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
logger.info("textDocument/didOpen", request)
|
||||
pushDiagnostics.delete(request.path)
|
||||
pullDiagnostics.delete(request.path)
|
||||
await connection.sendNotification("textDocument/didOpen", {
|
||||
@@ -679,11 +642,6 @@ export async function create(input: {
|
||||
const normalizedPath = Filesystem.normalizePath(
|
||||
path.isAbsolute(request.path) ? request.path : path.resolve(input.directory, request.path),
|
||||
)
|
||||
logger.info("waiting for diagnostics", {
|
||||
path: normalizedPath,
|
||||
mode: request.mode ?? "full",
|
||||
version: request.version,
|
||||
})
|
||||
if (request.mode === "document") {
|
||||
await waitForDocumentDiagnostics({ path: normalizedPath, version: request.version, after: request.after })
|
||||
return
|
||||
@@ -691,16 +649,11 @@ export async function create(input: {
|
||||
await waitForFullDiagnostics({ path: normalizedPath, version: request.version, after: request.after })
|
||||
},
|
||||
async shutdown() {
|
||||
logger.info("shutting down")
|
||||
connection.end()
|
||||
connection.dispose()
|
||||
await Process.stop(input.server.process)
|
||||
logger.info("shutdown")
|
||||
},
|
||||
}
|
||||
|
||||
logger.info("initialized")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Bus } from "@/bus"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import * as LSPClient from "./client"
|
||||
import path from "path"
|
||||
import { pathToFileURL, fileURLToPath } from "url"
|
||||
@@ -13,9 +12,6 @@ import { InstanceState } from "@/effect/instance-state"
|
||||
import { containsPath } from "@/project/instance-context"
|
||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
|
||||
const log = Log.create({ service: "lsp" })
|
||||
|
||||
export const Event = {
|
||||
Updated: BusEvent.define("lsp.updated", Schema.Struct({})),
|
||||
}
|
||||
@@ -101,7 +97,6 @@ const kinds = [
|
||||
const filterExperimentalServers = (servers: Record<string, LSPServer.Info>, flags: RuntimeFlags.Info) => {
|
||||
if (flags.experimentalLspTy) {
|
||||
if (servers["pyright"]) {
|
||||
log.info("LSP server pyright is disabled because OPENCODE_EXPERIMENTAL_LSP_TY is enabled")
|
||||
delete servers["pyright"]
|
||||
}
|
||||
} else {
|
||||
@@ -152,7 +147,7 @@ export const layer = Layer.effect(
|
||||
const servers: Record<string, LSPServer.Info> = {}
|
||||
|
||||
if (!cfg.lsp) {
|
||||
log.info("all LSPs are disabled")
|
||||
yield* Effect.logInfo("all LSPs are disabled").pipe(Effect.annotateLogs({ service: "lsp" }))
|
||||
} else {
|
||||
for (const server of Object.values(LSPServer)) {
|
||||
servers[server.id] = server
|
||||
@@ -164,7 +159,7 @@ export const layer = Layer.effect(
|
||||
for (const [name, item] of Object.entries(cfg.lsp)) {
|
||||
const existing = servers[name]
|
||||
if (item.disabled) {
|
||||
log.info(`LSP server ${name} is disabled`)
|
||||
yield* Effect.logInfo(`LSP server ${name} is disabled`).pipe(Effect.annotateLogs({ service: "lsp" }))
|
||||
delete servers[name]
|
||||
continue
|
||||
}
|
||||
@@ -184,11 +179,16 @@ export const layer = Layer.effect(
|
||||
}
|
||||
}
|
||||
|
||||
log.info("enabled LSP servers", {
|
||||
serverIds: Object.values(servers)
|
||||
.map((server) => server.id)
|
||||
.join(", "),
|
||||
})
|
||||
yield* Effect.logInfo("enabled LSP servers").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "lsp",
|
||||
...{
|
||||
serverIds: Object.values(servers)
|
||||
.map((server) => server.id)
|
||||
.join(", "),
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const s: State = {
|
||||
@@ -225,13 +225,10 @@ export const layer = Layer.effect(
|
||||
})
|
||||
.catch((err) => {
|
||||
s.broken.add(key)
|
||||
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
|
||||
return undefined
|
||||
})
|
||||
|
||||
if (!handle) return undefined
|
||||
log.info("spawned lsp server", { serverID: server.id, root })
|
||||
|
||||
const client = await LSPClient.create({
|
||||
serverID: server.id,
|
||||
server: handle,
|
||||
@@ -241,7 +238,6 @@ export const layer = Layer.effect(
|
||||
}).catch(async (err) => {
|
||||
s.broken.add(key)
|
||||
await Process.stop(handle.process)
|
||||
log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
|
||||
return undefined
|
||||
})
|
||||
|
||||
@@ -344,7 +340,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const touchFile = Effect.fn("LSP.touchFile")(function* (input: string, diagnostics?: "document" | "full") {
|
||||
log.info("touching file", { file: input })
|
||||
yield* Effect.logInfo("touching file").pipe(Effect.annotateLogs({ service: "lsp", ...{ file: input } }))
|
||||
const clients = yield* getClients(input)
|
||||
yield* Effect.promise(() =>
|
||||
Promise.all(
|
||||
@@ -359,9 +355,7 @@ export const layer = Layer.effect(
|
||||
after,
|
||||
})
|
||||
}),
|
||||
).catch((err) => {
|
||||
log.error("failed to touch file", { err, file: input })
|
||||
}),
|
||||
).catch((err) => {}),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { ChildProcessWithoutNullStreams } from "child_process"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { text } from "node:stream/consumers"
|
||||
import fs from "fs/promises"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
@@ -14,8 +13,6 @@ import { Module } from "@opencode-ai/core/util/module"
|
||||
import { spawn } from "./launch"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import type { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
|
||||
const log = Log.create({ service: "lsp.server" })
|
||||
const pathExists = async (p: string) =>
|
||||
fs
|
||||
.stat(p)
|
||||
@@ -80,7 +77,6 @@ export const Deno: Info = {
|
||||
async spawn(root) {
|
||||
const deno = which("deno")
|
||||
if (!deno) {
|
||||
log.info("deno not found, please install deno first")
|
||||
return
|
||||
}
|
||||
return {
|
||||
@@ -100,7 +96,6 @@ export const Typescript: Info = {
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
||||
async spawn(root, ctx) {
|
||||
const tsserver = Module.resolve("typescript/lib/tsserver.js", ctx.directory)
|
||||
log.info("typescript server", { tsserver })
|
||||
if (!tsserver) return
|
||||
const bin = await Npm.which("typescript-language-server")
|
||||
if (!bin) return
|
||||
@@ -157,11 +152,9 @@ export const ESLint: Info = {
|
||||
async spawn(root, ctx, flags) {
|
||||
const eslint = Module.resolve("eslint", ctx.directory)
|
||||
if (!eslint) return
|
||||
log.info("spawning eslint server")
|
||||
const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
|
||||
if (!(await Filesystem.exists(serverPath))) {
|
||||
if (flags.disableLspDownload) return
|
||||
log.info("downloading and building VS Code ESLint server")
|
||||
const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
|
||||
if (!response.ok) return
|
||||
|
||||
@@ -171,7 +164,6 @@ export const ESLint: Info = {
|
||||
const ok = await Archive.extractZip(zipPath, Global.Path.bin)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
log.error("Failed to extract vscode-eslint archive", { error })
|
||||
return false
|
||||
})
|
||||
if (!ok) return
|
||||
@@ -182,7 +174,6 @@ export const ESLint: Info = {
|
||||
|
||||
const stats = await fs.stat(finalPath).catch(() => undefined)
|
||||
if (stats) {
|
||||
log.info("removing old eslint installation", { path: finalPath })
|
||||
await fs.rm(finalPath, { force: true, recursive: true })
|
||||
}
|
||||
await fs.rename(extractedPath, finalPath)
|
||||
@@ -190,8 +181,6 @@ export const ESLint: Info = {
|
||||
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"
|
||||
await Process.run([npmCmd, "install"], { cwd: finalPath })
|
||||
await Process.run([npmCmd, "run", "compile"], { cwd: finalPath })
|
||||
|
||||
log.info("installed VS Code ESLint server", { serverPath })
|
||||
}
|
||||
|
||||
const proc = spawn("node", [serverPath, "--stdio"], {
|
||||
@@ -274,8 +263,6 @@ export const Oxlint: Info = {
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
log.info("oxlint not found, please install oxlint")
|
||||
return
|
||||
},
|
||||
}
|
||||
@@ -355,8 +342,6 @@ export const Gopls: Info = {
|
||||
if (!bin) {
|
||||
if (!which("go")) return
|
||||
if (flags.disableLspDownload) return
|
||||
|
||||
log.info("installing gopls")
|
||||
const proc = Process.spawn(["go", "install", "golang.org/x/tools/gopls@latest"], {
|
||||
env: { ...process.env, GOBIN: Global.Path.bin },
|
||||
stdout: "pipe",
|
||||
@@ -365,13 +350,9 @@ export const Gopls: Info = {
|
||||
})
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0) {
|
||||
log.error("Failed to install gopls")
|
||||
return
|
||||
}
|
||||
bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : ""))
|
||||
log.info(`installed gopls`, {
|
||||
bin,
|
||||
})
|
||||
}
|
||||
return {
|
||||
process: spawn(bin!, {
|
||||
@@ -391,11 +372,9 @@ export const Rubocop: Info = {
|
||||
const ruby = which("ruby")
|
||||
const gem = which("gem")
|
||||
if (!ruby || !gem) {
|
||||
log.info("Ruby not found, please install Ruby first")
|
||||
return
|
||||
}
|
||||
if (flags.disableLspDownload) return
|
||||
log.info("installing rubocop")
|
||||
const proc = Process.spawn(["gem", "install", "rubocop", "--bindir", Global.Path.bin], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
@@ -403,13 +382,9 @@ export const Rubocop: Info = {
|
||||
})
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0) {
|
||||
log.error("Failed to install rubocop")
|
||||
return
|
||||
}
|
||||
bin = path.join(Global.Path.bin, "rubocop" + (process.platform === "win32" ? ".exe" : ""))
|
||||
log.info(`installed rubocop`, {
|
||||
bin,
|
||||
})
|
||||
}
|
||||
return {
|
||||
process: spawn(bin!, ["--lsp"], {
|
||||
@@ -466,7 +441,6 @@ export const Ty: Info = {
|
||||
}
|
||||
|
||||
if (!binary) {
|
||||
log.error("ty not found, please install ty first")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -543,13 +517,10 @@ export const ElixirLS: Info = {
|
||||
if (!(await Filesystem.exists(binary))) {
|
||||
const elixir = which("elixir")
|
||||
if (!elixir) {
|
||||
log.error("elixir is required to run elixir-ls")
|
||||
return
|
||||
}
|
||||
|
||||
if (flags.disableLspDownload) return
|
||||
log.info("downloading elixir-ls from GitHub releases")
|
||||
|
||||
const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
|
||||
if (!response.ok) return
|
||||
const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
|
||||
@@ -558,7 +529,6 @@ export const ElixirLS: Info = {
|
||||
const ok = await Archive.extractZip(zipPath, Global.Path.bin)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
log.error("Failed to extract elixir-ls archive", { error })
|
||||
return false
|
||||
})
|
||||
if (!ok) return
|
||||
@@ -573,10 +543,6 @@ export const ElixirLS: Info = {
|
||||
await Process.run(["mix", "deps.get"], { cwd, env })
|
||||
await Process.run(["mix", "compile"], { cwd, env })
|
||||
await Process.run(["mix", "elixir_ls.release2", "-o", "release"], { cwd, env })
|
||||
|
||||
log.info(`installed elixir-ls`, {
|
||||
path: elixirLsPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,16 +564,12 @@ export const Zls: Info = {
|
||||
if (!bin) {
|
||||
const zig = which("zig")
|
||||
if (!zig) {
|
||||
log.error("Zig is required to use zls. Please install Zig first.")
|
||||
return
|
||||
}
|
||||
|
||||
if (flags.disableLspDownload) return
|
||||
log.info("downloading zls from GitHub releases")
|
||||
|
||||
const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
|
||||
if (!releaseResponse.ok) {
|
||||
log.error("Failed to fetch zls release info")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -644,20 +606,17 @@ export const Zls: Info = {
|
||||
]
|
||||
|
||||
if (!supportedCombos.includes(assetName)) {
|
||||
log.error(`Platform ${platform} and architecture ${arch} is not supported by zls`)
|
||||
return
|
||||
}
|
||||
|
||||
const asset = release.assets?.find((a) => a.name === assetName)
|
||||
if (!asset?.browser_download_url) {
|
||||
log.error(`Could not find asset ${assetName} in latest zls release`)
|
||||
return
|
||||
}
|
||||
|
||||
const downloadUrl = asset.browser_download_url
|
||||
const downloadResponse = await fetch(downloadUrl)
|
||||
if (!downloadResponse.ok) {
|
||||
log.error("Failed to download zls")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -668,7 +627,6 @@ export const Zls: Info = {
|
||||
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
log.error("Failed to extract zls archive", { error })
|
||||
return false
|
||||
})
|
||||
if (!ok) return
|
||||
@@ -681,15 +639,12 @@ export const Zls: Info = {
|
||||
bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : ""))
|
||||
|
||||
if (!(await Filesystem.exists(bin))) {
|
||||
log.error("Failed to extract zls binary")
|
||||
return
|
||||
}
|
||||
|
||||
if (platform !== "win32") {
|
||||
await fs.chmod(bin, 0o755).catch(() => {})
|
||||
}
|
||||
|
||||
log.info(`installed zls`, { bin })
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -726,11 +681,8 @@ export const Razor: Info = {
|
||||
|
||||
const razor = await findVscodeRazorExtension()
|
||||
if (!razor) {
|
||||
log.info("VS Code C# extension with Razor support not found, skipping Razor LSP")
|
||||
return
|
||||
}
|
||||
|
||||
log.info("using VS Code Razor extension for roslyn-language-server", { extension: razor.extension })
|
||||
return {
|
||||
process: spawn(
|
||||
bin,
|
||||
@@ -767,12 +719,10 @@ async function getRoslynLanguageServer(disableLspDownload: boolean) {
|
||||
|
||||
async function installRoslynLanguageServer(disableLspDownload: boolean) {
|
||||
if (!which("dotnet")) {
|
||||
log.error(".NET SDK is required to install roslyn-language-server")
|
||||
return
|
||||
}
|
||||
|
||||
if (disableLspDownload) return
|
||||
log.info("installing roslyn-language-server via dotnet tool")
|
||||
const proc = Process.spawn(["dotnet", "tool", "install", "--global", "roslyn-language-server", "--prerelease"], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
@@ -780,23 +730,18 @@ async function installRoslynLanguageServer(disableLspDownload: boolean) {
|
||||
})
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0) {
|
||||
log.error("Failed to install roslyn-language-server")
|
||||
return
|
||||
}
|
||||
|
||||
const resolved = which("roslyn-language-server")
|
||||
if (resolved) {
|
||||
log.info(`installed roslyn-language-server`, { bin: resolved })
|
||||
return resolved
|
||||
}
|
||||
|
||||
const global = await roslynLanguageServerGlobalPath()
|
||||
if (global) {
|
||||
log.info(`installed roslyn-language-server`, { bin: global })
|
||||
return global
|
||||
}
|
||||
|
||||
log.error("Installed roslyn-language-server but could not resolve executable")
|
||||
}
|
||||
|
||||
async function roslynLanguageServerGlobalPath() {
|
||||
@@ -853,12 +798,10 @@ export const FSharp: Info = {
|
||||
let bin = which("fsautocomplete")
|
||||
if (!bin) {
|
||||
if (!which("dotnet")) {
|
||||
log.error(".NET SDK is required to install fsautocomplete")
|
||||
return
|
||||
}
|
||||
|
||||
if (flags.disableLspDownload) return
|
||||
log.info("installing fsautocomplete via dotnet tool")
|
||||
const proc = Process.spawn(["dotnet", "tool", "install", "fsautocomplete", "--tool-path", Global.Path.bin], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
@@ -866,12 +809,10 @@ export const FSharp: Info = {
|
||||
})
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0) {
|
||||
log.error("Failed to install fsautocomplete")
|
||||
return
|
||||
}
|
||||
|
||||
bin = path.join(Global.Path.bin, "fsautocomplete" + (process.platform === "win32" ? ".exe" : ""))
|
||||
log.info(`installed fsautocomplete`, { bin })
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -951,7 +892,6 @@ export const RustAnalyzer: Info = {
|
||||
async spawn(root) {
|
||||
const bin = which("rust-analyzer")
|
||||
if (!bin) {
|
||||
log.info("rust-analyzer not found in path, please install it")
|
||||
return
|
||||
}
|
||||
return {
|
||||
@@ -1002,11 +942,8 @@ export const Clangd: Info = {
|
||||
}
|
||||
|
||||
if (flags.disableLspDownload) return
|
||||
log.info("downloading clangd from GitHub releases")
|
||||
|
||||
const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
|
||||
if (!releaseResponse.ok) {
|
||||
log.error("Failed to fetch clangd release info")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1017,7 +954,6 @@ export const Clangd: Info = {
|
||||
|
||||
const tag = release.tag_name
|
||||
if (!tag) {
|
||||
log.error("clangd release did not include a tag name")
|
||||
return
|
||||
}
|
||||
const platform = process.platform
|
||||
@@ -1028,7 +964,6 @@ export const Clangd: Info = {
|
||||
}
|
||||
const token = tokens[platform]
|
||||
if (!token) {
|
||||
log.error(`Platform ${platform} is not supported by clangd auto-download`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1045,21 +980,18 @@ export const Clangd: Info = {
|
||||
assets.find((item) => valid(item) && item.name?.endsWith(".tar.xz")) ??
|
||||
assets.find((item) => valid(item))
|
||||
if (!asset?.name || !asset.browser_download_url) {
|
||||
log.error("clangd could not match release asset", { tag, platform })
|
||||
return
|
||||
}
|
||||
|
||||
const name = asset.name
|
||||
const downloadResponse = await fetch(asset.browser_download_url)
|
||||
if (!downloadResponse.ok) {
|
||||
log.error("Failed to download clangd")
|
||||
return
|
||||
}
|
||||
|
||||
const archive = path.join(Global.Path.bin, name)
|
||||
const buf = await downloadResponse.arrayBuffer()
|
||||
if (buf.byteLength === 0) {
|
||||
log.error("Failed to write clangd archive")
|
||||
return
|
||||
}
|
||||
await Filesystem.write(archive, Buffer.from(buf))
|
||||
@@ -1067,7 +999,6 @@ export const Clangd: Info = {
|
||||
const zip = name.endsWith(".zip")
|
||||
const tar = name.endsWith(".tar.xz")
|
||||
if (!zip && !tar) {
|
||||
log.error("clangd encountered unsupported asset", { asset: name })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1075,7 +1006,6 @@ export const Clangd: Info = {
|
||||
const ok = await Archive.extractZip(archive, Global.Path.bin)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
log.error("Failed to extract clangd archive", { error })
|
||||
return false
|
||||
})
|
||||
if (!ok) return
|
||||
@@ -1087,7 +1017,6 @@ export const Clangd: Info = {
|
||||
|
||||
const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext)
|
||||
if (!(await Filesystem.exists(bin))) {
|
||||
log.error("Failed to extract clangd binary")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1097,9 +1026,6 @@ export const Clangd: Info = {
|
||||
|
||||
await fs.unlink(path.join(Global.Path.bin, "clangd")).catch(() => {})
|
||||
await fs.symlink(bin, path.join(Global.Path.bin, "clangd")).catch(() => {})
|
||||
|
||||
log.info(`installed clangd`, { bin })
|
||||
|
||||
return {
|
||||
process: spawn(bin, args, {
|
||||
cwd: root,
|
||||
@@ -1142,7 +1068,6 @@ export const Astro: Info = {
|
||||
async spawn(root, ctx, flags) {
|
||||
const tsserver = Module.resolve("typescript/lib/tsserver.js", ctx.directory)
|
||||
if (!tsserver) {
|
||||
log.info("typescript not found, required for Astro language server")
|
||||
return
|
||||
}
|
||||
const tsdk = path.dirname(tsserver)
|
||||
@@ -1203,7 +1128,6 @@ export const JDTLS: Info = {
|
||||
async spawn(root, _ctx, flags) {
|
||||
const java = which("java")
|
||||
if (!java) {
|
||||
log.error("Java 21 or newer is required to run the JDTLS. Please install it first.")
|
||||
return
|
||||
}
|
||||
const javaMajorVersion = await run(["java", "-version"]).then((result) => {
|
||||
@@ -1211,7 +1135,6 @@ export const JDTLS: Info = {
|
||||
return !m ? undefined : parseInt(m[1])
|
||||
})
|
||||
if (javaMajorVersion == null || javaMajorVersion < 21) {
|
||||
log.error("JDTLS requires at least Java 21.")
|
||||
return
|
||||
}
|
||||
const distPath = path.join(Global.Path.bin, "jdtls")
|
||||
@@ -1219,29 +1142,21 @@ export const JDTLS: Info = {
|
||||
const installed = await pathExists(launcherDir)
|
||||
if (!installed) {
|
||||
if (flags.disableLspDownload) return
|
||||
log.info("Downloading JDTLS LSP server.")
|
||||
await fs.mkdir(distPath, { recursive: true })
|
||||
const releaseURL =
|
||||
"https://www.eclipse.org/downloads/download.php?file=/jdtls/snapshots/jdt-language-server-latest.tar.gz"
|
||||
const archiveName = "release.tar.gz"
|
||||
|
||||
log.info("Downloading JDTLS archive", { url: releaseURL, dest: distPath })
|
||||
const download = await fetch(releaseURL)
|
||||
if (!download.ok || !download.body) {
|
||||
log.error("Failed to download JDTLS", { status: download.status, statusText: download.statusText })
|
||||
return
|
||||
}
|
||||
await Filesystem.writeStream(path.join(distPath, archiveName), download.body)
|
||||
|
||||
log.info("Extracting JDTLS archive")
|
||||
const tarResult = await run(["tar", "-xzf", archiveName], { cwd: distPath })
|
||||
if (tarResult.code !== 0) {
|
||||
log.error("Failed to extract JDTLS", { exitCode: tarResult.code, stderr: tarResult.stderr.toString() })
|
||||
return
|
||||
}
|
||||
|
||||
await fs.rm(path.join(distPath, archiveName), { force: true })
|
||||
log.info("JDTLS download and extraction completed")
|
||||
}
|
||||
const jarFileName =
|
||||
(await fs.readdir(launcherDir).catch(() => []))
|
||||
@@ -1249,7 +1164,6 @@ export const JDTLS: Info = {
|
||||
?.trim() ?? ""
|
||||
const launcherJar = path.join(launcherDir, jarFileName)
|
||||
if (!(await pathExists(launcherJar))) {
|
||||
log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`)
|
||||
return
|
||||
}
|
||||
const configFile = path.join(
|
||||
@@ -1317,11 +1231,8 @@ export const KotlinLS: Info = {
|
||||
const installed = await Filesystem.exists(launcherScript)
|
||||
if (!installed) {
|
||||
if (flags.disableLspDownload) return
|
||||
log.info("Downloading Kotlin Language Server from GitHub.")
|
||||
|
||||
const releaseResponse = await fetch("https://api.github.com/repos/Kotlin/kotlin-lsp/releases/latest")
|
||||
if (!releaseResponse.ok) {
|
||||
log.error("Failed to fetch kotlin-lsp release info")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1329,7 +1240,6 @@ export const KotlinLS: Info = {
|
||||
const version = release.name?.replace(/^v/, "")
|
||||
|
||||
if (!version) {
|
||||
log.error("Could not determine Kotlin LSP version from release")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1350,7 +1260,6 @@ export const KotlinLS: Info = {
|
||||
const combo = `${kotlinPlatform}-${kotlinArch}`
|
||||
|
||||
if (!supportedCombos.includes(combo)) {
|
||||
log.error(`Platform ${platform}/${arch} is not supported by Kotlin LSP`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1361,17 +1270,12 @@ export const KotlinLS: Info = {
|
||||
const archivePath = path.join(distPath, "kotlin-ls.zip")
|
||||
const download = await fetch(releaseURL)
|
||||
if (!download.ok || !download.body) {
|
||||
log.error("Failed to download Kotlin Language Server", {
|
||||
status: download.status,
|
||||
statusText: download.statusText,
|
||||
})
|
||||
return
|
||||
}
|
||||
await Filesystem.writeStream(archivePath, download.body)
|
||||
const ok = await Archive.extractZip(archivePath, distPath)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
log.error("Failed to extract Kotlin LS archive", { error })
|
||||
return false
|
||||
})
|
||||
if (!ok) return
|
||||
@@ -1379,10 +1283,8 @@ export const KotlinLS: Info = {
|
||||
if (process.platform !== "win32") {
|
||||
await fs.chmod(launcherScript, 0o755).catch(() => {})
|
||||
}
|
||||
log.info("Installed Kotlin Language Server", { path: launcherScript })
|
||||
}
|
||||
if (!(await Filesystem.exists(launcherScript))) {
|
||||
log.error(`Failed to locate the Kotlin LS launcher script in the installed directory: ${distPath}.`)
|
||||
return
|
||||
}
|
||||
return {
|
||||
@@ -1436,11 +1338,8 @@ export const LuaLS: Info = {
|
||||
|
||||
if (!bin) {
|
||||
if (flags.disableLspDownload) return
|
||||
log.info("downloading lua-language-server from GitHub releases")
|
||||
|
||||
const releaseResponse = await fetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest")
|
||||
if (!releaseResponse.ok) {
|
||||
log.error("Failed to fetch lua-language-server release info")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1475,20 +1374,17 @@ export const LuaLS: Info = {
|
||||
|
||||
const assetSuffix = `${lualsPlatform}-${lualsArch}.${ext}`
|
||||
if (!supportedCombos.includes(assetSuffix)) {
|
||||
log.error(`Platform ${platform} and architecture ${arch} is not supported by lua-language-server`)
|
||||
return
|
||||
}
|
||||
|
||||
const asset = release.assets.find((a: any) => a.name === assetName)
|
||||
if (!asset) {
|
||||
log.error(`Could not find asset ${assetName} in latest lua-language-server release`)
|
||||
return
|
||||
}
|
||||
|
||||
const downloadUrl = asset.browser_download_url
|
||||
const downloadResponse = await fetch(downloadUrl)
|
||||
if (!downloadResponse.ok) {
|
||||
log.error("Failed to download lua-language-server")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1512,7 +1408,6 @@ export const LuaLS: Info = {
|
||||
const ok = await Archive.extractZip(tempPath, installDir)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
log.error("Failed to extract lua-language-server archive", { error })
|
||||
return false
|
||||
})
|
||||
if (!ok) return
|
||||
@@ -1520,7 +1415,6 @@ export const LuaLS: Info = {
|
||||
const ok = await run(["tar", "-xzf", tempPath, "-C", installDir])
|
||||
.then((result) => result.code === 0)
|
||||
.catch((error: unknown) => {
|
||||
log.error("Failed to extract lua-language-server archive", { error })
|
||||
return false
|
||||
})
|
||||
if (!ok) return
|
||||
@@ -1532,7 +1426,6 @@ export const LuaLS: Info = {
|
||||
bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : ""))
|
||||
|
||||
if (!(await Filesystem.exists(bin))) {
|
||||
log.error("Failed to extract lua-language-server binary")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1541,15 +1434,10 @@ export const LuaLS: Info = {
|
||||
.chmod(bin, 0o755)
|
||||
.then(() => true)
|
||||
.catch((error: unknown) => {
|
||||
log.error("Failed to set executable permission for lua-language-server binary", {
|
||||
error,
|
||||
})
|
||||
return false
|
||||
})
|
||||
if (!ok) return
|
||||
}
|
||||
|
||||
log.info(`installed lua-language-server`, { bin })
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1598,7 +1486,6 @@ export const Prisma: Info = {
|
||||
async spawn(root) {
|
||||
const prisma = which("prisma")
|
||||
if (!prisma) {
|
||||
log.info("prisma not found, please install prisma")
|
||||
return
|
||||
}
|
||||
return {
|
||||
@@ -1616,7 +1503,6 @@ export const Dart: Info = {
|
||||
async spawn(root) {
|
||||
const dart = which("dart")
|
||||
if (!dart) {
|
||||
log.info("dart not found, please install dart first")
|
||||
return
|
||||
}
|
||||
return {
|
||||
@@ -1634,7 +1520,6 @@ export const Ocaml: Info = {
|
||||
async spawn(root) {
|
||||
const bin = which("ocamllsp")
|
||||
if (!bin) {
|
||||
log.info("ocamllsp not found, please install ocaml-lsp-server")
|
||||
return
|
||||
}
|
||||
return {
|
||||
@@ -1679,11 +1564,8 @@ export const TerraformLS: Info = {
|
||||
|
||||
if (!bin) {
|
||||
if (flags.disableLspDownload) return
|
||||
log.info("downloading terraform-ls from HashiCorp releases")
|
||||
|
||||
const releaseResponse = await fetch("https://api.releases.hashicorp.com/v1/releases/terraform-ls/latest")
|
||||
if (!releaseResponse.ok) {
|
||||
log.error("Failed to fetch terraform-ls release info")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1701,13 +1583,11 @@ export const TerraformLS: Info = {
|
||||
const builds = release.builds ?? []
|
||||
const build = builds.find((b) => b.arch === tfArch && b.os === tfPlatform)
|
||||
if (!build?.url) {
|
||||
log.error(`Could not find build for ${tfPlatform}/${tfArch} terraform-ls release version ${release.version}`)
|
||||
return
|
||||
}
|
||||
|
||||
const downloadResponse = await fetch(build.url)
|
||||
if (!downloadResponse.ok) {
|
||||
log.error("Failed to download terraform-ls")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1717,7 +1597,6 @@ export const TerraformLS: Info = {
|
||||
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
log.error("Failed to extract terraform-ls archive", { error })
|
||||
return false
|
||||
})
|
||||
if (!ok) return
|
||||
@@ -1726,15 +1605,12 @@ export const TerraformLS: Info = {
|
||||
bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : ""))
|
||||
|
||||
if (!(await Filesystem.exists(bin))) {
|
||||
log.error("Failed to extract terraform-ls binary")
|
||||
return
|
||||
}
|
||||
|
||||
if (platform !== "win32") {
|
||||
await fs.chmod(bin, 0o755).catch(() => {})
|
||||
}
|
||||
|
||||
log.info(`installed terraform-ls`, { bin })
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1760,11 +1636,8 @@ export const TexLab: Info = {
|
||||
|
||||
if (!bin) {
|
||||
if (flags.disableLspDownload) return
|
||||
log.info("downloading texlab from GitHub releases")
|
||||
|
||||
const response = await fetch("https://api.github.com/repos/latex-lsp/texlab/releases/latest")
|
||||
if (!response.ok) {
|
||||
log.error("Failed to fetch texlab release info")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1774,7 +1647,6 @@ export const TexLab: Info = {
|
||||
}
|
||||
const version = release.tag_name?.replace("v", "")
|
||||
if (!version) {
|
||||
log.error("texlab release did not include a version tag")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1789,13 +1661,11 @@ export const TexLab: Info = {
|
||||
const assets = release.assets ?? []
|
||||
const asset = assets.find((a) => a.name === assetName)
|
||||
if (!asset?.browser_download_url) {
|
||||
log.error(`Could not find asset ${assetName} in texlab release`)
|
||||
return
|
||||
}
|
||||
|
||||
const downloadResponse = await fetch(asset.browser_download_url)
|
||||
if (!downloadResponse.ok) {
|
||||
log.error("Failed to download texlab")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1806,7 +1676,6 @@ export const TexLab: Info = {
|
||||
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
log.error("Failed to extract texlab archive", { error })
|
||||
return false
|
||||
})
|
||||
if (!ok) return
|
||||
@@ -1820,15 +1689,12 @@ export const TexLab: Info = {
|
||||
bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : ""))
|
||||
|
||||
if (!(await Filesystem.exists(bin))) {
|
||||
log.error("Failed to extract texlab binary")
|
||||
return
|
||||
}
|
||||
|
||||
if (platform !== "win32") {
|
||||
await fs.chmod(bin, 0o755).catch(() => {})
|
||||
}
|
||||
|
||||
log.info("installed texlab", { bin })
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1872,7 +1738,6 @@ export const Gleam: Info = {
|
||||
async spawn(root) {
|
||||
const gleam = which("gleam")
|
||||
if (!gleam) {
|
||||
log.info("gleam not found, please install gleam first")
|
||||
return
|
||||
}
|
||||
return {
|
||||
@@ -1893,7 +1758,6 @@ export const Clojure: Info = {
|
||||
bin = which("clojure-lsp.exe")
|
||||
}
|
||||
if (!bin) {
|
||||
log.info("clojure-lsp not found, please install clojure-lsp first")
|
||||
return
|
||||
}
|
||||
return {
|
||||
@@ -1921,7 +1785,6 @@ export const Nixd: Info = {
|
||||
async spawn(root) {
|
||||
const nixd = which("nixd")
|
||||
if (!nixd) {
|
||||
log.info("nixd not found, please install nixd first")
|
||||
return
|
||||
}
|
||||
return {
|
||||
@@ -1944,11 +1807,8 @@ export const Tinymist: Info = {
|
||||
|
||||
if (!bin) {
|
||||
if (flags.disableLspDownload) return
|
||||
log.info("downloading tinymist from GitHub releases")
|
||||
|
||||
const response = await fetch("https://api.github.com/repos/Myriad-Dreamin/tinymist/releases/latest")
|
||||
if (!response.ok) {
|
||||
log.error("Failed to fetch tinymist release info")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1980,13 +1840,11 @@ export const Tinymist: Info = {
|
||||
const assets = release.assets ?? []
|
||||
const asset = assets.find((a) => a.name === assetName)
|
||||
if (!asset?.browser_download_url) {
|
||||
log.error(`Could not find asset ${assetName} in tinymist release`)
|
||||
return
|
||||
}
|
||||
|
||||
const downloadResponse = await fetch(asset.browser_download_url)
|
||||
if (!downloadResponse.ok) {
|
||||
log.error("Failed to download tinymist")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1997,7 +1855,6 @@ export const Tinymist: Info = {
|
||||
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
log.error("Failed to extract tinymist archive", { error })
|
||||
return false
|
||||
})
|
||||
if (!ok) return
|
||||
@@ -2010,15 +1867,12 @@ export const Tinymist: Info = {
|
||||
bin = path.join(Global.Path.bin, "tinymist" + (platform === "win32" ? ".exe" : ""))
|
||||
|
||||
if (!(await Filesystem.exists(bin))) {
|
||||
log.error("Failed to extract tinymist binary")
|
||||
return
|
||||
}
|
||||
|
||||
if (platform !== "win32") {
|
||||
await fs.chmod(bin, 0o755).catch(() => {})
|
||||
}
|
||||
|
||||
log.info("installed tinymist", { bin })
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -2034,7 +1888,6 @@ export const HLS: Info = {
|
||||
async spawn(root) {
|
||||
const bin = which("haskell-language-server-wrapper")
|
||||
if (!bin) {
|
||||
log.info("haskell-language-server-wrapper not found, please install haskell-language-server")
|
||||
return
|
||||
}
|
||||
return {
|
||||
@@ -2052,7 +1905,6 @@ export const JuliaLS: Info = {
|
||||
async spawn(root) {
|
||||
const julia = which("julia")
|
||||
if (!julia) {
|
||||
log.info("julia not found, please install julia first (https://julialang.org/downloads/)")
|
||||
return
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from "@modelcontextprotocol/sdk/types.js"
|
||||
import { Config } from "@/config/config"
|
||||
import { ConfigMCP } from "../config/mcp"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { withTimeout } from "@/util/timeout"
|
||||
@@ -31,8 +30,6 @@ import { EffectBridge } from "@/effect/bridge"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
|
||||
const log = Log.create({ service: "mcp" })
|
||||
const DEFAULT_TIMEOUT = 30_000
|
||||
|
||||
const TolerantListToolsResultSchema = ListToolsResultSchema.extend({
|
||||
@@ -116,7 +113,6 @@ const sanitize = (s: string) => s.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
|
||||
function remoteURL(key: string, value: string) {
|
||||
if (URL.canParse(value)) return new URL(value)
|
||||
log.warn("invalid remote mcp url", { key })
|
||||
}
|
||||
|
||||
function isOutputSchemaValidationError(error: Error) {
|
||||
@@ -133,8 +129,6 @@ function listTools(key: string, client: MCPClient, timeout: number) {
|
||||
Effect.map((result) => result.tools),
|
||||
Effect.catch((error) => {
|
||||
if (!isOutputSchemaValidationError(error)) return Effect.fail(error)
|
||||
|
||||
log.warn("failed to validate MCP tool output schemas, retrying without output schema validation", { key, error })
|
||||
return Effect.tryPromise({
|
||||
try: () =>
|
||||
client.request({ method: "tools/list" }, TolerantListToolsResultSchema, {
|
||||
@@ -188,7 +182,6 @@ function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number
|
||||
function defs(key: string, client: MCPClient, timeout?: number) {
|
||||
return listTools(key, client, timeout ?? DEFAULT_TIMEOUT).pipe(
|
||||
Effect.catch((err) => {
|
||||
log.error("failed to get tools from client", { key, error: err })
|
||||
return Effect.succeed(undefined)
|
||||
}),
|
||||
)
|
||||
@@ -203,7 +196,6 @@ function fetchFromClient<T extends { name: string }>(
|
||||
return Effect.tryPromise({
|
||||
try: () => listFn(client),
|
||||
catch: (e: any) => {
|
||||
log.error(`failed to get ${label}`, { clientName, error: e.message })
|
||||
return e
|
||||
},
|
||||
}).pipe(
|
||||
@@ -329,9 +321,7 @@ export const layer = Layer.effect(
|
||||
redirectUri: oauthConfig?.redirectUri,
|
||||
},
|
||||
{
|
||||
onRedirect: async (url) => {
|
||||
log.info("oauth redirect requested", { key, url: url.toString() })
|
||||
},
|
||||
onRedirect: async (url) => {},
|
||||
},
|
||||
auth,
|
||||
)
|
||||
@@ -366,8 +356,6 @@ export const layer = Layer.effect(
|
||||
error instanceof UnauthorizedError || (authProvider && lastError.message.includes("OAuth"))
|
||||
|
||||
if (isAuthError) {
|
||||
log.info("mcp server requires authentication", { key, transport: name })
|
||||
|
||||
if (lastError.message.includes("registration") || lastError.message.includes("client_id")) {
|
||||
lastStatus = {
|
||||
status: "needs_client_registration" as const,
|
||||
@@ -394,19 +382,14 @@ export const layer = Layer.effect(
|
||||
.pipe(Effect.ignore, Effect.as(undefined))
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("transport connection failed", {
|
||||
key,
|
||||
transport: name,
|
||||
url: mcp.url,
|
||||
error: lastError.message,
|
||||
})
|
||||
lastStatus = { status: "failed" as const, error: lastError.message }
|
||||
return Effect.succeed(undefined)
|
||||
}),
|
||||
)
|
||||
if (result) {
|
||||
log.info("connected", { key, transport: result.transportName })
|
||||
yield* Effect.logInfo("connected").pipe(
|
||||
Effect.annotateLogs({ service: "mcp", ...{ key, transport: result.transportName } }),
|
||||
)
|
||||
return { client: result.client as MCPClient | undefined, status: { status: "connected" } as Status }
|
||||
}
|
||||
// If this was an auth error, stop trying other transports
|
||||
@@ -436,9 +419,7 @@ export const layer = Layer.effect(
|
||||
...mcp.environment,
|
||||
},
|
||||
})
|
||||
transport.stderr?.on("data", (chunk: Buffer) => {
|
||||
log.info(`mcp stderr: ${chunk.toString()}`, { key })
|
||||
})
|
||||
transport.stderr?.on("data", (chunk: Buffer) => {})
|
||||
|
||||
const connectTimeout = mcp.timeout ?? DEFAULT_TIMEOUT
|
||||
return yield* connectTransport(transport, connectTimeout).pipe(
|
||||
@@ -448,7 +429,6 @@ export const layer = Layer.effect(
|
||||
})),
|
||||
Effect.catch((error): Effect.Effect<{ client: MCPClient | undefined; status: Status }> => {
|
||||
const msg = error instanceof Error ? error.message : String(error)
|
||||
log.error("local mcp startup failed", { key, command: mcp.command, cwd, error: msg })
|
||||
return Effect.succeed({ client: undefined, status: { status: "failed", error: msg } })
|
||||
}),
|
||||
)
|
||||
@@ -456,11 +436,11 @@ export const layer = Layer.effect(
|
||||
|
||||
const create = Effect.fn("MCP.create")(function* (key: string, mcp: ConfigMCP.Info) {
|
||||
if (mcp.enabled === false) {
|
||||
log.info("mcp server disabled", { key })
|
||||
yield* Effect.logInfo("mcp server disabled").pipe(Effect.annotateLogs({ service: "mcp", ...{ key } }))
|
||||
return DISABLED_RESULT
|
||||
}
|
||||
|
||||
log.info("found", { key, type: mcp.type })
|
||||
yield* Effect.logInfo("found").pipe(Effect.annotateLogs({ service: "mcp", ...{ key, type: mcp.type } }))
|
||||
|
||||
const { client: mcpClient, status } =
|
||||
mcp.type === "remote"
|
||||
@@ -477,7 +457,9 @@ export const layer = Layer.effect(
|
||||
return { status: { status: "failed", error: "Failed to get tools" } } satisfies CreateResult
|
||||
}
|
||||
|
||||
log.info("create() successfully created client", { key, toolCount: listed.length })
|
||||
yield* Effect.logInfo("create() successfully created client").pipe(
|
||||
Effect.annotateLogs({ service: "mcp", ...{ key, toolCount: listed.length } }),
|
||||
)
|
||||
return { mcpClient, status, defs: listed } satisfies CreateResult
|
||||
})
|
||||
const cfgSvc = yield* Config.Service
|
||||
@@ -508,7 +490,6 @@ export const layer = Layer.effect(
|
||||
|
||||
function watch(s: State, name: string, client: MCPClient, bridge: EffectBridge.Shape, timeout?: number) {
|
||||
client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
|
||||
log.info("tools list changed notification received", { server: name })
|
||||
if (s.clients[name] !== client || s.status[name]?.status !== "connected") return
|
||||
|
||||
const listed = await bridge.promise(defs(name, client, timeout))
|
||||
@@ -537,7 +518,9 @@ export const layer = Layer.effect(
|
||||
([key, mcp]) =>
|
||||
Effect.gen(function* () {
|
||||
if (!isMcpConfigured(mcp)) {
|
||||
log.error("Ignoring MCP config entry without type", { key })
|
||||
yield* Effect.logError("Ignoring MCP config entry without type").pipe(
|
||||
Effect.annotateLogs({ service: "mcp", ...{ key } }),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -688,7 +671,9 @@ export const layer = Layer.effect(
|
||||
|
||||
const listed = s.defs[clientName]
|
||||
if (!listed) {
|
||||
log.warn("missing cached tools for connected server", { clientName })
|
||||
yield* Effect.logWarning("missing cached tools for connected server").pipe(
|
||||
Effect.annotateLogs({ service: "mcp", ...{ clientName } }),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -734,13 +719,14 @@ export const layer = Layer.effect(
|
||||
const s = yield* InstanceState.get(state)
|
||||
const client = s.clients[clientName]
|
||||
if (!client) {
|
||||
log.warn(`client not found for ${label}`, { clientName })
|
||||
yield* Effect.logWarning(`client not found for ${label}`).pipe(
|
||||
Effect.annotateLogs({ service: "mcp", ...{ clientName } }),
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
return yield* Effect.tryPromise({
|
||||
try: () => fn(client),
|
||||
catch: (e: any) => {
|
||||
log.error(`failed to ${label}`, { clientName, ...meta, error: e?.message })
|
||||
return e
|
||||
},
|
||||
}).pipe(Effect.orElseSucceed(() => undefined))
|
||||
@@ -858,7 +844,9 @@ export const layer = Layer.effect(
|
||||
return yield* storeClient(s, mcpName, client, listed, mcpConfig.timeout)
|
||||
}
|
||||
|
||||
log.info("opening browser for oauth", { mcpName, url: result.authorizationUrl, state: result.oauthState })
|
||||
yield* Effect.logInfo("opening browser for oauth").pipe(
|
||||
Effect.annotateLogs({ service: "mcp", ...{ mcpName, url: result.authorizationUrl, state: result.oauthState } }),
|
||||
)
|
||||
|
||||
const callbackPromise = McpOAuthCallback.waitForCallback(result.oauthState, mcpName)
|
||||
|
||||
@@ -879,7 +867,6 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
),
|
||||
Effect.catch(() => {
|
||||
log.warn("failed to open browser, user must open URL manually", { mcpName })
|
||||
return bus.publish(BrowserOpenFailed, { mcpName, url: result.authorizationUrl }).pipe(Effect.ignore)
|
||||
}),
|
||||
)
|
||||
@@ -903,7 +890,6 @@ export const layer = Layer.effect(
|
||||
const result = yield* Effect.tryPromise({
|
||||
try: () => transport.finishAuth(authorizationCode).then(() => true as const),
|
||||
catch: (error) => {
|
||||
log.error("failed to finish oauth", { mcpName, error })
|
||||
return error
|
||||
},
|
||||
}).pipe(Effect.option)
|
||||
@@ -924,7 +910,7 @@ export const layer = Layer.effect(
|
||||
yield* auth.remove(mcpName)
|
||||
McpOAuthCallback.cancelPending(mcpName)
|
||||
pendingOAuthTransports.delete(mcpName)
|
||||
log.info("removed oauth credentials", { mcpName })
|
||||
yield* Effect.logInfo("removed oauth credentials").pipe(Effect.annotateLogs({ service: "mcp", ...{ mcpName } }))
|
||||
})
|
||||
|
||||
const supportsOAuth = Effect.fn("MCP.supportsOAuth")(function* (mcpName: string) {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { createConnection } from "net"
|
||||
import { createServer } from "http"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH, parseRedirectUri } from "./oauth-provider"
|
||||
|
||||
const log = Log.create({ service: "mcp.oauth-callback" })
|
||||
|
||||
// Current callback server configuration (may differ from defaults if custom redirectUri is used)
|
||||
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH, parseRedirectUri } from "./oauth-provider" // Current callback server configuration (may differ from defaults if custom redirectUri is used)
|
||||
let currentPort = OAUTH_CALLBACK_PORT
|
||||
let currentPath = OAUTH_CALLBACK_PATH
|
||||
|
||||
@@ -85,14 +80,9 @@ function handleRequest(req: import("http").IncomingMessage, res: import("http").
|
||||
const code = url.searchParams.get("code")
|
||||
const state = url.searchParams.get("state")
|
||||
const error = url.searchParams.get("error")
|
||||
const errorDescription = url.searchParams.get("error_description")
|
||||
|
||||
log.info("received oauth callback", { hasCode: !!code, state, error })
|
||||
|
||||
// Enforce state parameter presence
|
||||
const errorDescription = url.searchParams.get("error_description") // Enforce state parameter presence
|
||||
if (!state) {
|
||||
const errorMsg = "Missing required state parameter - potential CSRF attack"
|
||||
log.error("oauth callback missing state parameter", { url: url.toString() })
|
||||
res.writeHead(400, { "Content-Type": "text/html" })
|
||||
res.end(HTML_ERROR(errorMsg))
|
||||
return
|
||||
@@ -121,7 +111,6 @@ function handleRequest(req: import("http").IncomingMessage, res: import("http").
|
||||
// Validate state parameter
|
||||
if (!pendingAuths.has(state)) {
|
||||
const errorMsg = "Invalid or expired state parameter - potential CSRF attack"
|
||||
log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) })
|
||||
res.writeHead(400, { "Content-Type": "text/html" })
|
||||
res.end(HTML_ERROR(errorMsg))
|
||||
return
|
||||
@@ -144,7 +133,6 @@ export async function ensureRunning(redirectUri?: string): Promise<void> {
|
||||
|
||||
// If server is running on a different port/path, stop it first
|
||||
if (server && (currentPort !== port || currentPath !== path)) {
|
||||
log.info("stopping oauth callback server to reconfigure", { oldPort: currentPort, newPort: port })
|
||||
await stop()
|
||||
}
|
||||
|
||||
@@ -152,7 +140,6 @@ export async function ensureRunning(redirectUri?: string): Promise<void> {
|
||||
|
||||
const running = await isPortInUse(port)
|
||||
if (running) {
|
||||
log.info("oauth callback server already running on another instance", { port })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -162,7 +149,6 @@ export async function ensureRunning(redirectUri?: string): Promise<void> {
|
||||
server = createServer(handleRequest)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server!.listen(currentPort, () => {
|
||||
log.info("oauth callback server started", { port: currentPort, path: currentPath })
|
||||
resolve()
|
||||
})
|
||||
server!.on("error", reject)
|
||||
@@ -214,7 +200,6 @@ export async function stop(): Promise<void> {
|
||||
if (server) {
|
||||
await new Promise<void>((resolve) => server!.close(() => resolve()))
|
||||
server = undefined
|
||||
log.info("oauth callback server stopped")
|
||||
}
|
||||
|
||||
for (const [_name, pending] of pendingAuths) {
|
||||
|
||||
@@ -7,10 +7,6 @@ import type {
|
||||
} from "@modelcontextprotocol/sdk/shared/auth.js"
|
||||
import { Effect } from "effect"
|
||||
import { McpAuth } from "./auth"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
const log = Log.create({ service: "mcp.oauth" })
|
||||
|
||||
const OAUTH_CALLBACK_PORT = 19876
|
||||
const OAUTH_CALLBACK_PATH = "/mcp/oauth/callback"
|
||||
|
||||
@@ -70,7 +66,6 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||
if (entry?.clientInfo) {
|
||||
// Check if client secret has expired
|
||||
if (entry.clientInfo.clientSecretExpiresAt && entry.clientInfo.clientSecretExpiresAt < Date.now() / 1000) {
|
||||
log.info("client secret expired, need to re-register", { mcpName: this.mcpName })
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
@@ -96,10 +91,6 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||
this.serverUrl,
|
||||
),
|
||||
)
|
||||
log.info("saved dynamically registered client", {
|
||||
mcpName: this.mcpName,
|
||||
clientId: info.client_id,
|
||||
})
|
||||
}
|
||||
|
||||
async tokens(): Promise<OAuthTokens | undefined> {
|
||||
@@ -131,11 +122,9 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||
this.serverUrl,
|
||||
),
|
||||
)
|
||||
log.info("saved oauth tokens", { mcpName: this.mcpName })
|
||||
}
|
||||
|
||||
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
|
||||
log.info("redirecting to authorization", { mcpName: this.mcpName, url: authorizationUrl.toString() })
|
||||
await this.callbacks.onRedirect(authorizationUrl)
|
||||
}
|
||||
|
||||
@@ -173,7 +162,6 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||
}
|
||||
|
||||
async invalidateCredentials(type: "all" | "client" | "tokens"): Promise<void> {
|
||||
log.info("invalidating credentials", { mcpName: this.mcpName, type })
|
||||
const entry = await Effect.runPromise(this.auth.get(this.mcpName))
|
||||
if (!entry) {
|
||||
return
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export { Config } from "@/config/config"
|
||||
export { Server } from "./server/server"
|
||||
export { bootstrap } from "./cli/bootstrap"
|
||||
export * as Log from "@opencode-ai/core/util/log"
|
||||
export { Database } from "@/storage/db"
|
||||
export { JsonMigration } from "@/storage/json-migration"
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as path from "path"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import * as Bom from "../util/bom"
|
||||
|
||||
const log = Log.create({ service: "patch" })
|
||||
|
||||
export const PatchSchema = Schema.Struct({
|
||||
patchText: Schema.String.annotate({ description: "The full patch text that describes all changes to be made" }),
|
||||
})
|
||||
@@ -530,14 +526,14 @@ export const applyHunksToFiles = Effect.fn("Patch.applyHunksToFiles")(function*
|
||||
case "add": {
|
||||
yield* fs.writeWithDirs(hunk.path, hunk.contents)
|
||||
added.push(hunk.path)
|
||||
log.info(`Added file: ${hunk.path}`)
|
||||
yield* Effect.logInfo(`Added file: ${hunk.path}`).pipe(Effect.annotateLogs({ service: "patch" }))
|
||||
break
|
||||
}
|
||||
|
||||
case "delete": {
|
||||
yield* fs.remove(hunk.path)
|
||||
deleted.push(hunk.path)
|
||||
log.info(`Deleted file: ${hunk.path}`)
|
||||
yield* Effect.logInfo(`Deleted file: ${hunk.path}`).pipe(Effect.annotateLogs({ service: "patch" }))
|
||||
break
|
||||
}
|
||||
|
||||
@@ -549,11 +545,13 @@ export const applyHunksToFiles = Effect.fn("Patch.applyHunksToFiles")(function*
|
||||
yield* fs.writeWithDirs(hunk.move_path, Bom.join(fileUpdate.content, fileUpdate.bom))
|
||||
yield* fs.remove(hunk.path)
|
||||
modified.push(hunk.move_path)
|
||||
log.info(`Moved file: ${hunk.path} -> ${hunk.move_path}`)
|
||||
yield* Effect.logInfo(`Moved file: ${hunk.path} -> ${hunk.move_path}`).pipe(
|
||||
Effect.annotateLogs({ service: "patch" }),
|
||||
)
|
||||
} else {
|
||||
yield* fs.writeWithDirs(hunk.path, Bom.join(fileUpdate.content, fileUpdate.bom))
|
||||
modified.push(hunk.path)
|
||||
log.info(`Updated file: ${hunk.path}`)
|
||||
yield* Effect.logInfo(`Updated file: ${hunk.path}`).pipe(Effect.annotateLogs({ service: "patch" }))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -7,15 +7,11 @@ import { MessageID, SessionID } from "@/session/schema"
|
||||
import { PermissionTable } from "@/session/session.sql"
|
||||
import { Database } from "@/storage/db"
|
||||
import { eq } from "drizzle-orm"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Wildcard } from "@opencode-ai/core/util/wildcard"
|
||||
import { Deferred, Effect, Layer, Schema, Context } from "effect"
|
||||
import os from "os"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { PermissionID } from "./schema"
|
||||
|
||||
const log = Log.create({ service: "permission" })
|
||||
|
||||
export const Action = PermissionV2.Action.annotate({ identifier: "PermissionAction" })
|
||||
export type Action = Schema.Schema.Type<typeof Action>
|
||||
|
||||
@@ -175,7 +171,9 @@ export const layer = Layer.effect(
|
||||
|
||||
for (const pattern of request.patterns) {
|
||||
const rule = evaluate(request.permission, pattern, ruleset, approved)
|
||||
log.info("evaluated", { permission: request.permission, pattern, action: rule })
|
||||
yield* Effect.logInfo("evaluated").pipe(
|
||||
Effect.annotateLogs({ service: "permission", ...{ permission: request.permission, pattern, action: rule } }),
|
||||
)
|
||||
if (rule.action === "deny") {
|
||||
return yield* new DeniedError({
|
||||
ruleset: ruleset.filter((rule) => Wildcard.match(request.permission, rule.permission)),
|
||||
@@ -197,7 +195,9 @@ export const layer = Layer.effect(
|
||||
always: request.always,
|
||||
tool: request.tool,
|
||||
}
|
||||
log.info("asking", { id, permission: info.permission, patterns: info.patterns })
|
||||
yield* Effect.logInfo("asking").pipe(
|
||||
Effect.annotateLogs({ service: "permission", ...{ id, permission: info.permission, patterns: info.patterns } }),
|
||||
)
|
||||
|
||||
const deferred = yield* Deferred.make<void, RejectedError | CorrectedError>()
|
||||
pending.set(id, { info, deferred })
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { OAUTH_DUMMY_KEY } from "../auth"
|
||||
import os from "os"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { createServer } from "http"
|
||||
|
||||
const log = Log.create({ service: "plugin.codex" })
|
||||
|
||||
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
||||
const ISSUER = "https://auth.openai.com"
|
||||
const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
|
||||
@@ -323,7 +319,6 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
oauthServer!.listen(OAUTH_PORT, () => {
|
||||
log.info("codex oauth server started", { port: OAUTH_PORT })
|
||||
resolve()
|
||||
})
|
||||
oauthServer!.on("error", reject)
|
||||
@@ -334,9 +329,7 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
|
||||
|
||||
function stopOAuthServer() {
|
||||
if (oauthServer) {
|
||||
oauthServer.close(() => {
|
||||
log.info("codex oauth server stopped")
|
||||
})
|
||||
oauthServer.close(() => {})
|
||||
oauthServer = undefined
|
||||
}
|
||||
}
|
||||
@@ -444,7 +437,6 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
|
||||
// Check if token needs refresh
|
||||
if (!currentAuth.access || currentAuth.expires < Date.now()) {
|
||||
if (!refreshPromise) {
|
||||
log.info("refreshing codex access token")
|
||||
refreshPromise = refreshAccessToken(currentAuth.refresh, issuer)
|
||||
.then(async (tokens) => {
|
||||
const accountId = extractAccountId(tokens) || authWithAccount.accountId
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
|
||||
import type { Model } from "@opencode-ai/sdk/v2"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { createServer } from "http"
|
||||
import open from "open"
|
||||
|
||||
const log = Log.create({ service: "plugin.digitalocean" })
|
||||
|
||||
import { Effect } from "effect"
|
||||
const DO_OAUTH_CLIENT_ID = "b1a6c5158156caac821fd1b30253ca8acb52454a48fa744420e41889cb589f82"
|
||||
const DO_AUTHORIZE_URL = "https://cloud.digitalocean.com/v1/oauth/authorize"
|
||||
const DO_API_BASE = "https://api.digitalocean.com"
|
||||
@@ -188,7 +185,6 @@ async function startOAuthServer(): Promise<void> {
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
oauthServer!.listen(OAUTH_PORT, () => {
|
||||
log.info("digitalocean oauth server started", { port: OAUTH_PORT })
|
||||
resolve()
|
||||
})
|
||||
oauthServer!.on("error", reject)
|
||||
@@ -197,7 +193,9 @@ async function startOAuthServer(): Promise<void> {
|
||||
|
||||
function stopOAuthServer() {
|
||||
if (!oauthServer) return
|
||||
oauthServer.close(() => log.info("digitalocean oauth server stopped"))
|
||||
oauthServer.close(() =>
|
||||
Effect.logInfo("digitalocean oauth server stopped").pipe(Effect.annotateLogs({ service: "plugin.digitalocean" })),
|
||||
)
|
||||
oauthServer = undefined
|
||||
}
|
||||
|
||||
@@ -315,11 +313,13 @@ export async function DigitalOceanAuthPlugin(input: PluginInput): Promise<Hooks>
|
||||
path: { id: "digitalocean" },
|
||||
body: { type: "api", key: ctx.auth.key, metadata: updated },
|
||||
})
|
||||
.catch((err) => log.warn("failed to persist refreshed routers", { error: err }))
|
||||
.catch((err) =>
|
||||
Effect.logWarning("failed to persist refreshed routers").pipe(
|
||||
Effect.annotateLogs({ service: "plugin.digitalocean", ...{ error: err } }),
|
||||
),
|
||||
)
|
||||
} else if (result.status === 401 || result.status === 403) {
|
||||
log.warn("digitalocean oauth bearer rejected; using cached routers", { status: result.status })
|
||||
} else if (result.status !== 0) {
|
||||
log.warn("digitalocean router refresh failed", { status: result.status })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +355,6 @@ export async function DigitalOceanAuthPlugin(input: PluginInput): Promise<Hooks>
|
||||
const routerResult = await listRouters(tokens.access_token)
|
||||
const routers = routerResult.ok ? routerResult.routers : []
|
||||
if (!routerResult.ok) {
|
||||
log.warn("digitalocean initial router fetch failed", { status: routerResult.status })
|
||||
}
|
||||
return {
|
||||
type: "success" as const,
|
||||
@@ -372,7 +371,6 @@ export async function DigitalOceanAuthPlugin(input: PluginInput): Promise<Hooks>
|
||||
},
|
||||
}
|
||||
} catch (err) {
|
||||
log.error("digitalocean oauth callback failed", { error: err })
|
||||
return { type: "failed" as const }
|
||||
} finally {
|
||||
stopOAuthServer()
|
||||
|
||||
@@ -2,13 +2,9 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin"
|
||||
import type { Model } from "@opencode-ai/sdk/v2"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { iife } from "@/util/iife"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { CopilotModels } from "./models"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
|
||||
const log = Log.create({ service: "plugin.copilot" })
|
||||
|
||||
const CLIENT_ID = "Ov23li8tweQw6odWQebz"
|
||||
// Add a small safety buffer when polling to avoid hitting the server
|
||||
// slightly too early due to clock skew / timer drift.
|
||||
@@ -74,7 +70,6 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
},
|
||||
provider.models,
|
||||
).catch((error) => {
|
||||
log.error("failed to fetch copilot models", { error })
|
||||
return Object.fromEntries(
|
||||
Object.entries(provider.models).map(([id, model]) => [id, fix(model, base(auth.enterpriseUrl))]),
|
||||
)
|
||||
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
} from "@opencode-ai/plugin"
|
||||
import { Config } from "@/config/config"
|
||||
import { Bus } from "../bus"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||
import { ServerAuth } from "@/server/auth"
|
||||
import { CodexAuthPlugin } from "./codex"
|
||||
@@ -29,9 +28,6 @@ import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } fro
|
||||
import { registerAdapter } from "@/control-plane/adapters"
|
||||
import type { WorkspaceAdapter } from "@/control-plane/types"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
|
||||
const log = Log.create({ service: "plugin" })
|
||||
|
||||
type State = {
|
||||
hooks: Hooks[]
|
||||
}
|
||||
@@ -152,19 +148,28 @@ export const layer = Layer.effect(
|
||||
}
|
||||
|
||||
for (const plugin of flags.disableDefaultPlugins ? [] : INTERNAL_PLUGINS) {
|
||||
log.info("loading internal plugin", { name: plugin.name })
|
||||
yield* Effect.logInfo("loading internal plugin").pipe(
|
||||
Effect.annotateLogs({ service: "plugin", ...{ name: plugin.name } }),
|
||||
)
|
||||
const init = yield* Effect.tryPromise({
|
||||
try: () => plugin(input),
|
||||
catch: (err) => {
|
||||
log.error("failed to load internal plugin", { name: plugin.name, error: err })
|
||||
},
|
||||
}).pipe(Effect.option)
|
||||
catch: (error) => error,
|
||||
}).pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.logError("failed to load internal plugin").pipe(
|
||||
Effect.annotateLogs({ service: "plugin", ...{ name: plugin.name, error } }),
|
||||
),
|
||||
),
|
||||
Effect.option,
|
||||
)
|
||||
if (init._tag === "Some") hooks.push(init.value)
|
||||
}
|
||||
|
||||
const plugins = flags.pure ? [] : (cfg.plugin_origins ?? [])
|
||||
if (flags.pure && cfg.plugin_origins?.length) {
|
||||
log.info("skipping external plugins in pure mode", { count: cfg.plugin_origins.length })
|
||||
yield* Effect.logInfo("skipping external plugins in pure mode").pipe(
|
||||
Effect.annotateLogs({ service: "plugin", ...{ count: cfg.plugin_origins.length } }),
|
||||
)
|
||||
}
|
||||
if (plugins.length) yield* config.waitForDependencies()
|
||||
|
||||
@@ -173,12 +178,8 @@ export const layer = Layer.effect(
|
||||
items: plugins,
|
||||
kind: "server",
|
||||
report: {
|
||||
start(candidate) {
|
||||
log.info("loading plugin", { path: candidate.plan.spec })
|
||||
},
|
||||
missing(candidate, _retry, message) {
|
||||
log.warn("plugin has no server entrypoint", { path: candidate.plan.spec, message })
|
||||
},
|
||||
start(candidate) {},
|
||||
missing(candidate, _retry, message) {},
|
||||
error(candidate, _retry, stage, error, resolved) {
|
||||
const spec = candidate.plan.spec
|
||||
const cause = error instanceof Error ? (error.cause ?? error) : error
|
||||
@@ -186,24 +187,19 @@ export const layer = Layer.effect(
|
||||
|
||||
if (stage === "install") {
|
||||
const parsed = parsePluginSpecifier(spec)
|
||||
log.error("failed to install plugin", { pkg: parsed.pkg, version: parsed.version, error: message })
|
||||
publishPluginError(`Failed to install plugin ${parsed.pkg}@${parsed.version}: ${message}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (stage === "compatibility") {
|
||||
log.warn("plugin incompatible", { path: spec, error: message })
|
||||
publishPluginError(`Plugin ${spec} skipped: ${message}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (stage === "entry") {
|
||||
log.error("failed to resolve plugin server entry", { path: spec, error: message })
|
||||
publishPluginError(`Failed to load plugin ${spec}: ${message}`)
|
||||
return
|
||||
}
|
||||
|
||||
log.error("failed to load plugin", { path: spec, target: resolved?.entry, error: message })
|
||||
publishPluginError(`Failed to load plugin ${spec}: ${message}`)
|
||||
},
|
||||
},
|
||||
@@ -218,10 +214,14 @@ export const layer = Layer.effect(
|
||||
try: () => applyPlugin(load, input, hooks),
|
||||
catch: (err) => {
|
||||
const message = errorMessage(err)
|
||||
log.error("failed to load plugin", { path: load.spec, error: message })
|
||||
return message
|
||||
},
|
||||
}).pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.logError("failed to load plugin").pipe(
|
||||
Effect.annotateLogs({ service: "plugin", ...{ path: load.spec, error } }),
|
||||
),
|
||||
),
|
||||
Effect.catch(() => {
|
||||
// TODO: make proper events for this
|
||||
// bus.publish(Session.Event.Error, {
|
||||
@@ -238,10 +238,13 @@ export const layer = Layer.effect(
|
||||
for (const hook of hooks) {
|
||||
yield* Effect.tryPromise({
|
||||
try: () => Promise.resolve((hook as any).config?.(cfg)),
|
||||
catch: (err) => {
|
||||
log.error("plugin config hook failed", { error: err })
|
||||
},
|
||||
}).pipe(Effect.ignore)
|
||||
catch: (error) => error,
|
||||
}).pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.logError("plugin config hook failed").pipe(Effect.annotateLogs({ service: "plugin", ...{ error } })),
|
||||
),
|
||||
Effect.ignore,
|
||||
)
|
||||
}
|
||||
|
||||
// Subscribe to bus events, fiber interrupted when scope closes
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { OAUTH_DUMMY_KEY } from "../auth"
|
||||
import { createServer } from "http"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
|
||||
const log = Log.create({ service: "plugin.xai" })
|
||||
|
||||
import { Effect } from "effect"
|
||||
// Public Grok-CLI OAuth client. xAI's auth server rejects loopback OAuth from
|
||||
// non-allowlisted clients, so we reuse the Grok-CLI client_id that xAI ships
|
||||
// for desktop OAuth flows. Source of truth: hermes-agent PR #26534.
|
||||
@@ -510,8 +507,7 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
|
||||
// behavior and crash the entire opencode process. Matches the silent-
|
||||
// swallow behavior the Codex plugin gets from its permanent
|
||||
// `oauthServer!.on("error", reject)`.
|
||||
server.on("error", (err) => log.warn("xai oauth server error", { error: err }))
|
||||
log.info("xai oauth server started", { host: OAUTH_HOST, port: OAUTH_PORT })
|
||||
server.on("error", () => {})
|
||||
resolve()
|
||||
})
|
||||
oauthServer = server
|
||||
@@ -522,7 +518,9 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
|
||||
|
||||
function stopOAuthServer() {
|
||||
if (oauthServer) {
|
||||
oauthServer.close(() => log.info("xai oauth server stopped"))
|
||||
oauthServer.close(() =>
|
||||
Effect.logInfo("xai oauth server stopped").pipe(Effect.annotateLogs({ service: "plugin.xai" })),
|
||||
)
|
||||
oauthServer = undefined
|
||||
}
|
||||
}
|
||||
@@ -607,7 +605,6 @@ export async function XaiAuthPlugin(input: PluginInput, options: XaiAuthPluginOp
|
||||
if (expiresSoon) {
|
||||
if (!refreshPromise) {
|
||||
const refreshToken = currentAuth.refresh
|
||||
log.info("refreshing xai access token")
|
||||
refreshPromise = refreshAccessToken(refreshToken, options)
|
||||
.then(async (tokens) => {
|
||||
const refreshedExpires = Date.now() + (tokens.expires_in ?? 3600) * 1000
|
||||
@@ -627,7 +624,11 @@ export async function XaiAuthPlugin(input: PluginInput, options: XaiAuthPluginOp
|
||||
expires: refreshedExpires,
|
||||
},
|
||||
})
|
||||
.catch((err) => log.warn("failed to persist refreshed xai tokens", { error: err }))
|
||||
.catch((err) =>
|
||||
Effect.logWarning("failed to persist refreshed xai tokens").pipe(
|
||||
Effect.annotateLogs({ service: "plugin.xai", ...{ error: err } }),
|
||||
),
|
||||
)
|
||||
return { access: tokens.access_token, refresh: refreshedRefresh, expires: refreshedExpires }
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -688,7 +689,6 @@ export async function XaiAuthPlugin(input: PluginInput, options: XaiAuthPluginOp
|
||||
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
||||
}
|
||||
} catch (err) {
|
||||
log.error("xai oauth callback failed", { error: err })
|
||||
return { type: "failed" as const }
|
||||
} finally {
|
||||
stopOAuthServer()
|
||||
@@ -725,7 +725,6 @@ export async function XaiAuthPlugin(input: PluginInput, options: XaiAuthPluginOp
|
||||
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
||||
}
|
||||
} catch (err) {
|
||||
log.error("xai device code callback failed", { error: err })
|
||||
return { type: "failed" as const }
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Database } from "@/storage/db"
|
||||
import { ProjectTable } from "./project.sql"
|
||||
import { PermissionTable, SessionTable } from "../session/session.sql"
|
||||
import { WorkspaceTable } from "../control-plane/workspace.sql"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
@@ -21,9 +20,6 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { AbsolutePath, NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
|
||||
const log = Log.create({ service: "project" })
|
||||
|
||||
const ProjectVcs = Schema.Literal("git")
|
||||
|
||||
const ProjectIcon = Schema.Struct({
|
||||
@@ -233,7 +229,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const fromDirectory = Effect.fn("Project.fromDirectory")(function* (directory: string) {
|
||||
log.info("fromDirectory", { directory })
|
||||
yield* Effect.logInfo("fromDirectory").pipe(Effect.annotateLogs({ service: "project", ...{ directory } }))
|
||||
|
||||
const data = yield* projectV2.resolve(AbsolutePath.make(directory))
|
||||
const worktree = data.id === ProjectV2.ID.make("global") && !data.vcs ? "/" : data.directory
|
||||
|
||||
@@ -5,9 +5,6 @@ import { BusEvent } from "@/bus/bus-event"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { FileWatcher } from "@/file/watcher"
|
||||
import { Git } from "@/git"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
const log = Log.create({ service: "vcs" })
|
||||
const PATCH_CONTEXT_LINES = 2_147_483_647
|
||||
const MAX_PATCH_BYTES = 10_000_000
|
||||
const MAX_TOTAL_PATCH_BYTES = 10_000_000
|
||||
@@ -107,7 +104,10 @@ const batchPatches = Effect.fnUntraced(function* (
|
||||
context: options?.context ?? PATCH_CONTEXT_LINES,
|
||||
maxOutputBytes: MAX_TOTAL_PATCH_BYTES,
|
||||
})
|
||||
if (result.truncated) log.warn("batched patch exceeded byte limit", { max: MAX_TOTAL_PATCH_BYTES })
|
||||
if (result.truncated)
|
||||
yield* Effect.logWarning("batched patch exceeded byte limit").pipe(
|
||||
Effect.annotateLogs({ service: "vcs", ...{ max: MAX_TOTAL_PATCH_BYTES } }),
|
||||
)
|
||||
|
||||
return {
|
||||
patches: splitGitPatch(result).reduce((acc, patch, index) => {
|
||||
@@ -139,13 +139,15 @@ const nativePatch = Effect.fnUntraced(function* (
|
||||
})
|
||||
if (!result.truncated && result.text) return result.text
|
||||
|
||||
if (result.truncated) log.warn("patch exceeded byte limit", { file: item.file, max: MAX_PATCH_BYTES })
|
||||
if (result.truncated)
|
||||
yield* Effect.logWarning("patch exceeded byte limit").pipe(
|
||||
Effect.annotateLogs({ service: "vcs", ...{ file: item.file, max: MAX_PATCH_BYTES } }),
|
||||
)
|
||||
return emptyPatch(item.file)
|
||||
})
|
||||
|
||||
const totalPatch = (file: string, patch: string, total: number) => {
|
||||
if (total + Buffer.byteLength(patch) <= MAX_TOTAL_PATCH_BYTES) return { patch, capped: false }
|
||||
log.warn("total patch budget exceeded", { file, max: MAX_TOTAL_PATCH_BYTES })
|
||||
return { patch: emptyPatch(file), capped: true }
|
||||
}
|
||||
|
||||
@@ -325,7 +327,9 @@ export const layer: Layer.Layer<Service, never, Git.Service | Bus.Service> = Lay
|
||||
concurrency: 2,
|
||||
})
|
||||
const value = { current, root }
|
||||
log.info("initialized", { branch: value.current, default_branch: value.root?.name })
|
||||
yield* Effect.logInfo("initialized").pipe(
|
||||
Effect.annotateLogs({ service: "vcs", ...{ branch: value.current, default_branch: value.root?.name } }),
|
||||
)
|
||||
|
||||
yield* (yield* bus.subscribe(FileWatcher.Event.Updated)).pipe(
|
||||
Stream.filter((evt) => evt.properties.file.endsWith("HEAD")),
|
||||
@@ -333,7 +337,9 @@ export const layer: Layer.Layer<Service, never, Git.Service | Bus.Service> = Lay
|
||||
Effect.gen(function* () {
|
||||
const next = yield* get()
|
||||
if (next !== value.current) {
|
||||
log.info("branch changed", { from: value.current, to: next })
|
||||
yield* Effect.logInfo("branch changed").pipe(
|
||||
Effect.annotateLogs({ service: "vcs", ...{ from: value.current, to: next } }),
|
||||
)
|
||||
value.current = next
|
||||
yield* bus.publish(Event.BranchUpdated, { branch: next })
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import fuzzysort from "fuzzysort"
|
||||
import { Config } from "@/config/config"
|
||||
import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"
|
||||
import { NoSuchModelError, type Provider as SDK } from "ai"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import { Hash } from "@opencode-ai/core/util/hash"
|
||||
import { Plugin } from "../plugin"
|
||||
@@ -28,9 +27,6 @@ import * as ProviderTransform from "./transform"
|
||||
import { ModelID, ProviderID } from "./schema"
|
||||
import { ModelStatus } from "./model-status"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
|
||||
const log = Log.create({ service: "provider" })
|
||||
|
||||
function shouldUseCopilotResponsesApi(modelID: string): boolean {
|
||||
const match = /^gpt-(\d+)/.exec(modelID)
|
||||
if (!match) return false
|
||||
@@ -625,7 +621,6 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
|
||||
},
|
||||
async discoverModels(): Promise<Record<string, Model>> {
|
||||
if (!apiKey) {
|
||||
log.info("gitlab model discovery skipped: no apiKey")
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -633,19 +628,9 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
|
||||
const token = apiKey
|
||||
const getHeaders = (): Record<string, string> =>
|
||||
auth?.type === "api" ? { "PRIVATE-TOKEN": token } : { Authorization: `Bearer ${token}` }
|
||||
|
||||
log.info("gitlab model discovery starting", { instanceUrl })
|
||||
const result = await discoverWorkflowModels({ instanceUrl, getHeaders }, { workingDirectory: directory })
|
||||
|
||||
if (!result.models.length) {
|
||||
log.info("gitlab model discovery skipped: no models found", {
|
||||
project: result.project
|
||||
? {
|
||||
id: result.project.id,
|
||||
path: result.project.pathWithNamespace,
|
||||
}
|
||||
: null,
|
||||
})
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -693,14 +678,8 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("gitlab model discovery complete", {
|
||||
count: Object.keys(models).length,
|
||||
models: Object.keys(models),
|
||||
})
|
||||
return models
|
||||
} catch (e) {
|
||||
log.warn("gitlab model discovery failed", { error: e })
|
||||
return {}
|
||||
}
|
||||
},
|
||||
@@ -1200,7 +1179,6 @@ export const layer = Layer.effect(
|
||||
|
||||
const state = yield* InstanceState.make<State>(() =>
|
||||
Effect.gen(function* () {
|
||||
using _ = log.time("state")
|
||||
const bridge = yield* EffectBridge.make()
|
||||
const cfg = yield* config.get()
|
||||
const modelsDev = yield* modelsDevSvc.get()
|
||||
@@ -1226,7 +1204,7 @@ export const layer = Layer.effect(
|
||||
get: (key: string) => env.get(key),
|
||||
}
|
||||
|
||||
log.info("init")
|
||||
yield* Effect.logInfo("init").pipe(Effect.annotateLogs({ service: "provider" }))
|
||||
|
||||
function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
|
||||
const existing = providers[providerID]
|
||||
@@ -1428,7 +1406,9 @@ export const layer = Layer.effect(
|
||||
if (disabled.has(providerID)) continue
|
||||
const data = database[providerID]
|
||||
if (!data) {
|
||||
log.error("Provider does not exist in model list " + providerID)
|
||||
yield* Effect.logError("Provider does not exist in model list " + providerID).pipe(
|
||||
Effect.annotateLogs({ service: "provider" }),
|
||||
)
|
||||
continue
|
||||
}
|
||||
const result = yield* fn(data)
|
||||
@@ -1462,9 +1442,7 @@ export const layer = Layer.effect(
|
||||
providers[gitlab].models[modelID] = model
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn("state discovery error", { id: "gitlab", error: e })
|
||||
}
|
||||
} catch (e) {}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1516,7 +1494,7 @@ export const layer = Layer.effect(
|
||||
continue
|
||||
}
|
||||
|
||||
log.info("found", { providerID })
|
||||
yield* Effect.logInfo("found").pipe(Effect.annotateLogs({ service: "provider", ...{ providerID } }))
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1534,9 +1512,6 @@ export const layer = Layer.effect(
|
||||
|
||||
async function resolveSDK(model: Model, s: State, envs: Record<string, string | undefined>) {
|
||||
try {
|
||||
using _ = log.time("getSDK", {
|
||||
providerID: model.providerID,
|
||||
})
|
||||
const provider = s.providers[model.providerID]
|
||||
const options = { ...provider.options }
|
||||
|
||||
@@ -1647,10 +1622,6 @@ export const layer = Layer.effect(
|
||||
|
||||
const bundledLoader = BUNDLED_PROVIDERS[model.api.npm]
|
||||
if (bundledLoader) {
|
||||
log.info("using bundled provider", {
|
||||
providerID: model.providerID,
|
||||
pkg: model.api.npm,
|
||||
})
|
||||
const factory = await bundledLoader()
|
||||
const loaded = factory({
|
||||
name: model.providerID,
|
||||
@@ -1666,7 +1637,6 @@ export const layer = Layer.effect(
|
||||
if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`)
|
||||
installedPath = item.entrypoint
|
||||
} else {
|
||||
log.info("loading local provider", { pkg: model.api.npm })
|
||||
installedPath = model.api.npm
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,9 @@ import { lazy } from "@opencode-ai/core/util/lazy"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { Shell } from "@/shell/shell"
|
||||
import type { Proc } from "#pty"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { PtyID } from "./schema"
|
||||
import { Effect, Layer, Context, Schema, Types } from "effect"
|
||||
import { NonNegativeInt, PositiveInt } from "@opencode-ai/core/schema"
|
||||
|
||||
const log = Log.create({ service: "pty" })
|
||||
|
||||
const BUFFER_LIMIT = 1024 * 1024 * 2
|
||||
const BUFFER_CHUNK = 64 * 1024
|
||||
const encoder = new TextEncoder()
|
||||
@@ -167,7 +163,7 @@ export const layer = Layer.effect(
|
||||
const s = yield* InstanceState.get(state)
|
||||
const session = yield* requireSession(id)
|
||||
s.sessions.delete(id)
|
||||
log.info("removing session", { id })
|
||||
yield* Effect.logInfo("removing session").pipe(Effect.annotateLogs({ service: "pty", ...{ id } }))
|
||||
teardown(session)
|
||||
yield* bus.publish(Event.Deleted, { id: session.info.id })
|
||||
})
|
||||
@@ -207,7 +203,9 @@ export const layer = Layer.effect(
|
||||
env.LC_CTYPE = "C.UTF-8"
|
||||
env.LANG = "C.UTF-8"
|
||||
}
|
||||
log.info("creating session", { id, cmd: command, args, cwd })
|
||||
yield* Effect.logInfo("creating session").pipe(
|
||||
Effect.annotateLogs({ service: "pty", ...{ id, cmd: command, args, cwd } }),
|
||||
)
|
||||
|
||||
const { spawn } = yield* Effect.promise(() => pty())
|
||||
const proc = yield* Effect.sync(() =>
|
||||
@@ -262,9 +260,7 @@ export const layer = Layer.effect(
|
||||
session.bufferCursor += excess
|
||||
})
|
||||
proc.onExit(({ exitCode }) => {
|
||||
if (session.info.status === "exited") return
|
||||
log.info("session exited", { id, exitCode })
|
||||
session.info.status = "exited"
|
||||
if (session.info.status === "exited") return (session.info.status = "exited")
|
||||
bridge.fork(bus.publish(Event.Exited, { id, exitCode }))
|
||||
bridge.fork(remove(id))
|
||||
})
|
||||
@@ -306,7 +302,7 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
),
|
||||
)
|
||||
log.info("client connected to session", { id })
|
||||
yield* Effect.logInfo("client connected to session").pipe(Effect.annotateLogs({ service: "pty", ...{ id } }))
|
||||
|
||||
const sub = sock(ws)
|
||||
session.subscribers.delete(sub)
|
||||
@@ -354,7 +350,6 @@ export const layer = Layer.effect(
|
||||
session.process.write(typeof message === "string" ? message : new TextDecoder().decode(message))
|
||||
},
|
||||
onClose: () => {
|
||||
log.info("client disconnected from session", { id })
|
||||
cleanup()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,11 +3,7 @@ import { Bus } from "@/bus"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { QuestionID } from "./schema"
|
||||
|
||||
const log = Log.create({ service: "question" })
|
||||
|
||||
// Schemas — these are pure data; nothing checks class identity (see PR
|
||||
// description) so they're plain `Schema.Struct` + type alias. That lets
|
||||
// `Question.ask` and other internal sites trust the type contract without a
|
||||
@@ -159,7 +155,9 @@ export const layer = Layer.effect(
|
||||
}) {
|
||||
const pending = (yield* InstanceState.get(state)).pending
|
||||
const id = QuestionID.ascending()
|
||||
log.info("asking", { id, questions: input.questions.length })
|
||||
yield* Effect.logInfo("asking").pipe(
|
||||
Effect.annotateLogs({ service: "question", ...{ id, questions: input.questions.length } }),
|
||||
)
|
||||
|
||||
const deferred = yield* Deferred.make<ReadonlyArray<Answer>, RejectedError>()
|
||||
const info: Request = {
|
||||
@@ -186,11 +184,15 @@ export const layer = Layer.effect(
|
||||
const pending = (yield* InstanceState.get(state)).pending
|
||||
const existing = pending.get(input.requestID)
|
||||
if (!existing) {
|
||||
log.warn("reply for unknown request", { requestID: input.requestID })
|
||||
yield* Effect.logWarning("reply for unknown request").pipe(
|
||||
Effect.annotateLogs({ service: "question", ...{ requestID: input.requestID } }),
|
||||
)
|
||||
return yield* new NotFoundError({ requestID: input.requestID })
|
||||
}
|
||||
pending.delete(input.requestID)
|
||||
log.info("replied", { requestID: input.requestID, answers: input.answers })
|
||||
yield* Effect.logInfo("replied").pipe(
|
||||
Effect.annotateLogs({ service: "question", ...{ requestID: input.requestID, answers: input.answers } }),
|
||||
)
|
||||
yield* bus.publish(Event.Replied, {
|
||||
sessionID: existing.info.sessionID,
|
||||
requestID: existing.info.id,
|
||||
@@ -203,11 +205,13 @@ export const layer = Layer.effect(
|
||||
const pending = (yield* InstanceState.get(state)).pending
|
||||
const existing = pending.get(requestID)
|
||||
if (!existing) {
|
||||
log.warn("reject for unknown request", { requestID })
|
||||
yield* Effect.logWarning("reject for unknown request").pipe(
|
||||
Effect.annotateLogs({ service: "question", ...{ requestID } }),
|
||||
)
|
||||
return yield* new NotFoundError({ requestID })
|
||||
}
|
||||
pending.delete(requestID)
|
||||
log.info("rejected", { requestID })
|
||||
yield* Effect.logInfo("rejected").pipe(Effect.annotateLogs({ service: "question", ...{ requestID } }))
|
||||
yield* bus.publish(Event.Rejected, {
|
||||
sessionID: existing.info.sessionID,
|
||||
requestID: existing.info.id,
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Effect } from "effect"
|
||||
import { Event } from "./event"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
export const emitGlobalDisposed = Effect.sync(() =>
|
||||
GlobalBus.emit("event", {
|
||||
directory: "global",
|
||||
@@ -21,13 +17,7 @@ export const disposeAllInstancesAndEmitGlobalDisposed = Effect.fn("Server.dispos
|
||||
const store = yield* InstanceStore.Service
|
||||
yield* Effect.gen(function* () {
|
||||
yield* options?.swallowErrors
|
||||
? store.disposeAll().pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.sync(() => {
|
||||
log.warn("global disposal failed", { cause })
|
||||
}),
|
||||
),
|
||||
)
|
||||
? store.disposeAll().pipe(Effect.catchCause((cause) => Effect.sync(() => {})))
|
||||
: store.disposeAll()
|
||||
yield* emitGlobalDisposed
|
||||
}).pipe(Effect.uninterruptible)
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Bonjour } from "bonjour-service"
|
||||
|
||||
const log = Log.create({ service: "mdns" })
|
||||
|
||||
let bonjour: Bonjour | undefined
|
||||
let currentPort: number | undefined
|
||||
|
||||
@@ -22,17 +18,12 @@ export function publish(port: number, domain?: string) {
|
||||
txt: { path: "/" },
|
||||
})
|
||||
|
||||
service.on("up", () => {
|
||||
log.info("mDNS service published", { name, port })
|
||||
})
|
||||
service.on("up", () => {})
|
||||
|
||||
service.on("error", (err) => {
|
||||
log.error("mDNS service error", { error: err })
|
||||
})
|
||||
service.on("error", (err) => {})
|
||||
|
||||
currentPort = port
|
||||
} catch (err) {
|
||||
log.error("mDNS publish failed", { error: err })
|
||||
if (bonjour) {
|
||||
try {
|
||||
bonjour.destroy()
|
||||
@@ -48,12 +39,9 @@ export function unpublish() {
|
||||
try {
|
||||
bonjour.unpublishAll()
|
||||
bonjour.destroy()
|
||||
} catch (err) {
|
||||
log.error("mDNS unpublish failed", { error: err })
|
||||
}
|
||||
} catch (err) {}
|
||||
bonjour = undefined
|
||||
currentPort = undefined
|
||||
log.info("mDNS service unpublished")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Auth } from "@/auth"
|
||||
import { ProviderID } from "@/provider/schema"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { RootHttpApi } from "../api"
|
||||
@@ -24,8 +23,13 @@ export const controlHandlers = HttpApiBuilder.group(RootHttpApi, "control", (han
|
||||
})
|
||||
|
||||
const log = Effect.fn("ControlHttpApi.log")(function* (ctx: { payload: typeof LogInput.Type }) {
|
||||
const logger = Log.create({ service: ctx.payload.service })
|
||||
logger[ctx.payload.level](ctx.payload.message, ctx.payload.extra)
|
||||
const entry = (() => {
|
||||
if (ctx.payload.level === "debug") return Effect.logDebug(ctx.payload.message)
|
||||
if (ctx.payload.level === "warn") return Effect.logWarning(ctx.payload.message)
|
||||
if (ctx.payload.level === "error") return Effect.logError(ctx.payload.message)
|
||||
return Effect.logInfo(ctx.payload.message)
|
||||
})()
|
||||
yield* entry.pipe(Effect.annotateLogs({ service: ctx.payload.service, ...ctx.payload.extra }))
|
||||
return true
|
||||
})
|
||||
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { Bus } from "@/bus"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Effect } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import * as Sse from "effect/unstable/encoding/Sse"
|
||||
import { EventApi } from "../groups/event"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
function eventData(data: unknown): Sse.Event {
|
||||
return {
|
||||
_tag: "Event",
|
||||
@@ -32,14 +28,14 @@ function eventResponse(bus: Bus.Interface) {
|
||||
Stream.map(() => ({ id: Bus.createID(), type: "server.heartbeat", properties: {} })),
|
||||
)
|
||||
|
||||
log.info("event connected")
|
||||
yield* Effect.logInfo("event connected").pipe(Effect.annotateLogs({ service: "server" }))
|
||||
return HttpServerResponse.stream(
|
||||
Stream.make({ id: Bus.createID(), type: "server.connected", properties: {} }).pipe(
|
||||
Stream.concat(events.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }))),
|
||||
Stream.map(eventData),
|
||||
Stream.pipeThroughChannel(Sse.encode()),
|
||||
Stream.encodeText,
|
||||
Stream.ensuring(Effect.sync(() => log.info("event disconnected"))),
|
||||
Stream.ensuring(Effect.logInfo("event disconnected").pipe(Effect.annotateLogs({ service: "server" }))),
|
||||
),
|
||||
{
|
||||
contentType: "text/event-stream",
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Bus } from "@/bus"
|
||||
import { Installation } from "@/installation"
|
||||
import { disposeAllInstancesAndEmitGlobalDisposed } from "@/server/global-lifecycle"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Effect, Queue, Schema } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||
@@ -13,9 +12,6 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import * as Sse from "effect/unstable/encoding/Sse"
|
||||
import { RootHttpApi } from "../api"
|
||||
import { GlobalUpgradeInput } from "../groups/global"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
function eventData(data: unknown): Sse.Event {
|
||||
return {
|
||||
_tag: "Event",
|
||||
@@ -34,7 +30,6 @@ function parseBody(body: string) {
|
||||
}
|
||||
|
||||
function eventResponse() {
|
||||
log.info("global event connected")
|
||||
const events = Stream.callback<GlobalBusEvent>((queue) => {
|
||||
const handler = (event: GlobalBusEvent) => Queue.offerUnsafe(queue, event)
|
||||
return Effect.acquireRelease(
|
||||
@@ -53,7 +48,7 @@ function eventResponse() {
|
||||
Stream.map(eventData),
|
||||
Stream.pipeThroughChannel(Sse.encode()),
|
||||
Stream.encodeText,
|
||||
Stream.ensuring(Effect.sync(() => log.info("global event disconnected"))),
|
||||
Stream.ensuring(Effect.logInfo("global event disconnected").pipe(Effect.annotateLogs({ service: "server" }))),
|
||||
),
|
||||
{
|
||||
contentType: "text/event-stream",
|
||||
|
||||
@@ -14,10 +14,6 @@ import { Effect, Scope } from "effect"
|
||||
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../api"
|
||||
import { HistoryPayload, ReplayPayload, SessionPayload } from "../groups/sync"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
const log = Log.create({ service: "server.sync" })
|
||||
|
||||
export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const workspace = yield* Workspace.Service
|
||||
@@ -40,20 +36,30 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl
|
||||
data: { ...event.data },
|
||||
}))
|
||||
const source = events[0].aggregateID
|
||||
log.info("sync replay requested", {
|
||||
sessionID: source,
|
||||
events: events.length,
|
||||
first: events[0]?.seq,
|
||||
last: events.at(-1)?.seq,
|
||||
directory: ctx.payload.directory,
|
||||
})
|
||||
yield* Effect.logInfo("sync replay requested").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "server.sync",
|
||||
...{
|
||||
sessionID: source,
|
||||
events: events.length,
|
||||
first: events[0]?.seq,
|
||||
last: events.at(-1)?.seq,
|
||||
directory: ctx.payload.directory,
|
||||
},
|
||||
}),
|
||||
)
|
||||
yield* sync.replayAll(events)
|
||||
log.info("sync replay complete", {
|
||||
sessionID: source,
|
||||
events: events.length,
|
||||
first: events[0]?.seq,
|
||||
last: events.at(-1)?.seq,
|
||||
})
|
||||
yield* Effect.logInfo("sync replay complete").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "server.sync",
|
||||
...{
|
||||
sessionID: source,
|
||||
events: events.length,
|
||||
first: events[0]?.seq,
|
||||
last: events.at(-1)?.seq,
|
||||
},
|
||||
}),
|
||||
)
|
||||
return { sessionID: source }
|
||||
})
|
||||
|
||||
@@ -68,10 +74,15 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl
|
||||
},
|
||||
})
|
||||
|
||||
log.info("sync session stolen", {
|
||||
sessionID: ctx.payload.sessionID,
|
||||
workspaceID,
|
||||
})
|
||||
yield* Effect.logInfo("sync session stolen").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "server.sync",
|
||||
...{
|
||||
sessionID: ctx.payload.sessionID,
|
||||
workspaceID,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
return { sessionID: ctx.payload.sessionID }
|
||||
})
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import type { InstanceContext } from "@/project/instance-context"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Effect } from "effect"
|
||||
import { HttpEffect, HttpMiddleware, HttpServerRequest } from "effect/unstable/http"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
type MarkedInstance = {
|
||||
ctx: InstanceContext
|
||||
store: InstanceStore.Interface
|
||||
@@ -51,7 +47,11 @@ export const disposeMiddleware: HttpMiddleware.HttpMiddleware = (effect) =>
|
||||
if (!marked) return response
|
||||
disposeAfterResponse.delete(request.source)
|
||||
yield* Effect.uninterruptible(marked.bridge.run(marked.store.dispose(marked.ctx))).pipe(
|
||||
Effect.catchCause((cause) => Effect.sync(() => log.warn("instance disposal failed", { cause }))),
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.sync(() =>
|
||||
Effect.logWarning("instance disposal failed").pipe(Effect.annotateLogs({ service: "server", ...{ cause } })),
|
||||
),
|
||||
),
|
||||
)
|
||||
return response
|
||||
})
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Cause, Effect } from "effect"
|
||||
import { HttpRouter, HttpServerError, HttpServerRespondable, HttpServerResponse } from "effect/unstable/http"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
// Keep typed HttpApi failures on their declared error path; this boundary only replaces defect-only empty 500s.
|
||||
export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect) =>
|
||||
effect.pipe(
|
||||
@@ -19,9 +15,6 @@ export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect)
|
||||
|
||||
const error = defect.defect
|
||||
const ref = `err_${crypto.randomUUID().slice(0, 8)}`
|
||||
|
||||
log.error("failed", { ref, error, cause: Cause.pretty(cause) })
|
||||
|
||||
return Effect.succeed(
|
||||
HttpServerResponse.jsonUnsafe(
|
||||
new NamedError.Unknown({
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import { HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { InvalidRequestError } from "../errors"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
// Effect's Issue formatter recursively dumps the rejected `actual` value with
|
||||
// no truncation, so a 5KB invalid array produces a ~360KB string. Cap to keep
|
||||
// 4xx responses small and avoid mirroring entire request payloads (which may
|
||||
@@ -27,7 +23,6 @@ export class SchemaErrorMiddleware extends HttpApiMiddleware.Service<SchemaError
|
||||
|
||||
export const schemaErrorLayer = HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrorMiddleware, (error, context) => {
|
||||
const reason = truncateReason(error.cause.message)
|
||||
log.warn("schema rejection", { kind: error.kind, reason })
|
||||
if (context.endpoint.path.startsWith("/api/")) {
|
||||
return Effect.fail(
|
||||
new InvalidRequestError({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "./init-projectors"
|
||||
|
||||
import { NodeHttpServer } from "@effect/platform-node"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { ConfigProvider, Context, Effect, Exit, Layer, Scope } from "effect"
|
||||
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import { OpenApi } from "effect/unstable/httpapi"
|
||||
@@ -16,9 +15,6 @@ import { lazy } from "@/util/lazy"
|
||||
|
||||
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
|
||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
export type Listener = {
|
||||
hostname: string
|
||||
port: number
|
||||
@@ -165,7 +161,10 @@ function setupMdns(opts: ListenOptions, port: number, scope: Scope.Scope) {
|
||||
yield* Scope.addFinalizer(scope, unpublish)
|
||||
return unpublish
|
||||
}
|
||||
if (opts.mdns) log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
|
||||
if (opts.mdns)
|
||||
yield* Effect.logWarning("mDNS enabled but hostname is loopback; skipping mDNS publish").pipe(
|
||||
Effect.annotateLogs({ service: "server" }),
|
||||
)
|
||||
return Effect.void
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,13 +3,10 @@ import { inArray } from "drizzle-orm"
|
||||
import { EventSequenceTable } from "@/sync/event.sql"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import type { WorkspaceID } from "@/control-plane/schema"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export const HEADER = "x-opencode-sync"
|
||||
export type State = Record<string, number>
|
||||
const log = Log.create({ service: "fence" })
|
||||
|
||||
export function load(ids?: string[]) {
|
||||
const rows = Database.use((db) => {
|
||||
if (!ids?.length) {
|
||||
@@ -55,14 +52,24 @@ export function parse(headers: Headers): State | undefined {
|
||||
|
||||
export function wait(workspaceID: WorkspaceID, state: State, signal?: AbortSignal) {
|
||||
return Effect.gen(function* () {
|
||||
log.info("waiting for state", {
|
||||
workspaceID,
|
||||
state,
|
||||
})
|
||||
yield* Effect.logInfo("waiting for state").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "fence",
|
||||
...{
|
||||
workspaceID,
|
||||
state,
|
||||
},
|
||||
}),
|
||||
)
|
||||
yield* Workspace.Service.use((workspace) => workspace.waitForSync(workspaceID, state, signal))
|
||||
log.info("state fully synced", {
|
||||
workspaceID,
|
||||
state,
|
||||
})
|
||||
yield* Effect.logInfo("state fully synced").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "fence",
|
||||
...{
|
||||
workspaceID,
|
||||
state,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { SessionID, MessageID, PartID } from "./schema"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { Token } from "@/util/token"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { SessionProcessor } from "./processor"
|
||||
import { Agent } from "@/agent/agent"
|
||||
import { Plugin } from "@/plugin"
|
||||
@@ -20,9 +19,6 @@ import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { SessionEvent } from "@opencode-ai/core/session-event"
|
||||
|
||||
const log = Log.create({ service: "session.compaction" })
|
||||
|
||||
export const Event = {
|
||||
Compacted: BusEvent.define(
|
||||
"session.compacted",
|
||||
@@ -282,7 +278,10 @@ export const layer = Layer.effect(
|
||||
estimate,
|
||||
})
|
||||
if (split) keep = split
|
||||
else if (!keep) log.info("tail fallback", { budget, size, total })
|
||||
else if (!keep)
|
||||
yield* Effect.logInfo("tail fallback").pipe(
|
||||
Effect.annotateLogs({ service: "session.compaction", ...{ budget, size, total } }),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -298,7 +297,7 @@ export const layer = Layer.effect(
|
||||
const prune = Effect.fn("SessionCompaction.prune")(function* (input: { sessionID: SessionID }) {
|
||||
const cfg = yield* config.get()
|
||||
if (!cfg.compaction?.prune) return
|
||||
log.info("pruning")
|
||||
yield* Effect.logInfo("pruning").pipe(Effect.annotateLogs({ service: "session.compaction" }))
|
||||
|
||||
const msgs = yield* session
|
||||
.messages({ sessionID: input.sessionID })
|
||||
@@ -329,7 +328,7 @@ export const layer = Layer.effect(
|
||||
}
|
||||
}
|
||||
|
||||
log.info("found", { pruned, total })
|
||||
yield* Effect.logInfo("found").pipe(Effect.annotateLogs({ service: "session.compaction", ...{ pruned, total } }))
|
||||
if (pruned > PRUNE_MINIMUM) {
|
||||
for (const part of toPrune) {
|
||||
if (part.state.status === "completed") {
|
||||
@@ -337,7 +336,9 @@ export const layer = Layer.effect(
|
||||
yield* session.updatePart(part)
|
||||
}
|
||||
}
|
||||
log.info("pruned", { count: toPrune.length })
|
||||
yield* Effect.logInfo("pruned").pipe(
|
||||
Effect.annotateLogs({ service: "session.compaction", ...{ count: toPrune.length } }),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { streamText, wrapLanguageModel, type ModelMessage, type Tool } from "ai"
|
||||
@@ -26,8 +25,6 @@ import * as OtelTracer from "@effect/opentelemetry/Tracer"
|
||||
import { LLMAISDK } from "./llm/ai-sdk"
|
||||
import { LLMNativeRuntime } from "./llm/native-runtime"
|
||||
import { LLMRequestPrep } from "./llm/request"
|
||||
|
||||
const log = Log.create({ service: "llm" })
|
||||
export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX
|
||||
|
||||
export type StreamInput = {
|
||||
@@ -79,18 +76,16 @@ const live: Layer.Layer<
|
||||
const flags = yield* RuntimeFlags.Service
|
||||
|
||||
const run = Effect.fn("LLM.run")(function* (input: StreamRequest) {
|
||||
const l = log
|
||||
.clone()
|
||||
.tag("providerID", input.model.providerID)
|
||||
.tag("modelID", input.model.id)
|
||||
.tag("session.id", input.sessionID)
|
||||
.tag("small", (input.small ?? false).toString())
|
||||
.tag("agent", input.agent.name)
|
||||
.tag("mode", input.agent.mode)
|
||||
l.info("stream", {
|
||||
modelID: input.model.id,
|
||||
const logAnnotations = {
|
||||
service: "llm",
|
||||
providerID: input.model.providerID,
|
||||
})
|
||||
modelID: input.model.id,
|
||||
"session.id": input.sessionID,
|
||||
small: (input.small ?? false).toString(),
|
||||
agent: input.agent.name,
|
||||
mode: input.agent.mode,
|
||||
}
|
||||
yield* Effect.logInfo("stream").pipe(Effect.annotateLogs(logAnnotations))
|
||||
|
||||
const [language, cfg, item, info] = yield* Effect.all(
|
||||
[
|
||||
@@ -255,7 +250,9 @@ const live: Layer.Layer<
|
||||
"llm.native_unsupported_reason": native.reason,
|
||||
}),
|
||||
)
|
||||
l.info("native runtime unavailable; falling back to ai-sdk", { reason: native.reason })
|
||||
yield* Effect.logInfo("native runtime unavailable; falling back to ai-sdk").pipe(
|
||||
Effect.annotateLogs({ ...logAnnotations, reason: native.reason }),
|
||||
)
|
||||
}
|
||||
|
||||
yield* Effect.logInfo("llm runtime selected").pipe(
|
||||
@@ -270,18 +267,15 @@ const live: Layer.Layer<
|
||||
return {
|
||||
type: "ai-sdk" as const,
|
||||
result: streamText({
|
||||
onError(error) {
|
||||
l.error("stream error", {
|
||||
error,
|
||||
})
|
||||
},
|
||||
onError(error) {},
|
||||
async experimental_repairToolCall(failed) {
|
||||
const lower = failed.toolCall.toolName.toLowerCase()
|
||||
if (lower !== failed.toolCall.toolName && prepared.tools[lower]) {
|
||||
l.info("repairing tool call", {
|
||||
tool: failed.toolCall.toolName,
|
||||
repaired: lower,
|
||||
})
|
||||
await Effect.runPromise(
|
||||
Effect.logInfo("repairing tool call").pipe(
|
||||
Effect.annotateLogs({ ...logAnnotations, tool: failed.toolCall.toolName, repaired: lower }),
|
||||
),
|
||||
)
|
||||
return {
|
||||
...failed.toolCall,
|
||||
toolName: lower,
|
||||
|
||||
@@ -19,7 +19,6 @@ import { SessionSummary } from "./summary"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { Question } from "@/question"
|
||||
import { errorMessage } from "@/util/error"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { SessionEvent } from "@opencode-ai/core/session-event"
|
||||
@@ -30,8 +29,6 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { Usage, type LLMEvent } from "@opencode-ai/llm"
|
||||
|
||||
const DOOM_LOOP_THRESHOLD = 3
|
||||
const log = Log.create({ service: "session.processor" })
|
||||
|
||||
export type Result = "compact" | "stop" | "continue"
|
||||
|
||||
export interface Handle {
|
||||
@@ -120,7 +117,11 @@ export const layer = Layer.effect(
|
||||
reasoningMap: {},
|
||||
}
|
||||
let aborted = false
|
||||
const slog = log.clone().tag("session.id", input.sessionID).tag("messageID", input.assistantMessage.id)
|
||||
const logAnnotations = {
|
||||
service: "session.processor",
|
||||
"session.id": input.sessionID,
|
||||
messageID: input.assistantMessage.id,
|
||||
}
|
||||
|
||||
const parse = (e: unknown) =>
|
||||
MessageV2.fromError(e, {
|
||||
@@ -749,7 +750,13 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const halt = Effect.fn("SessionProcessor.halt")(function* (e: unknown) {
|
||||
slog.error("process", { error: errorMessage(e), stack: e instanceof Error ? e.stack : undefined })
|
||||
yield* Effect.logError("process").pipe(
|
||||
Effect.annotateLogs({
|
||||
...logAnnotations,
|
||||
error: errorMessage(e),
|
||||
stack: e instanceof Error ? e.stack : undefined,
|
||||
}),
|
||||
)
|
||||
const error = parse(e)
|
||||
if (MessageV2.ContextOverflowError.isInstance(error)) {
|
||||
ctx.needsCompaction = true
|
||||
@@ -778,7 +785,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) {
|
||||
slog.info("process")
|
||||
yield* Effect.logInfo("process").pipe(Effect.annotateLogs(logAnnotations))
|
||||
ctx.needsCompaction = false
|
||||
ctx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== true
|
||||
|
||||
|
||||
@@ -8,11 +8,7 @@ import * as Session from "./session"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { SessionTable, MessageTable, PartTable } from "./session.sql"
|
||||
import { WorkspaceTable } from "@/control-plane/workspace.sql"
|
||||
import { Log } from "@opencode-ai/core/util/log"
|
||||
import nextProjectors from "./projectors-next"
|
||||
|
||||
const log = Log.create({ service: "session.projector" })
|
||||
|
||||
function foreign(err: unknown) {
|
||||
if (typeof err !== "object" || err === null) return false
|
||||
if ("code" in err && err.code === "SQLITE_CONSTRAINT_FOREIGNKEY") return true
|
||||
@@ -138,7 +134,6 @@ export default [
|
||||
.run()
|
||||
} catch (err) {
|
||||
if (!foreign(err)) throw err
|
||||
log.warn("ignored late message update", { messageID: id, sessionID })
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -191,7 +186,6 @@ export default [
|
||||
if (next) applyUsage(db, sessionID, next)
|
||||
} catch (err) {
|
||||
if (!foreign(err)) throw err
|
||||
log.warn("ignored late part update", { partID: id, messageID, sessionID })
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import path from "path"
|
||||
import os from "os"
|
||||
import { SessionID, MessageID, PartID } from "./schema"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { SessionRevert } from "./revert"
|
||||
import * as Session from "./session"
|
||||
import { Agent } from "../agent/agent"
|
||||
@@ -42,7 +41,6 @@ import { Image } from "@/image/image"
|
||||
import { decodeDataUrl } from "@/util/data-url"
|
||||
import { Process } from "@/util/process"
|
||||
import { Cause, Effect, Exit, Latch, Layer, Option, Scope, Context, Schema, Types } from "effect"
|
||||
import * as EffectLogger from "@opencode-ai/core/effect/logger"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { TaskTool, type TaskPromptOps } from "@/tool/task"
|
||||
import { SessionRunState } from "./run-state"
|
||||
@@ -77,10 +75,6 @@ IMPORTANT:
|
||||
- This tool provides your final answer - no further actions are taken after calling it`
|
||||
|
||||
const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested structured output. You MUST use the StructuredOutput tool to provide your final response. Do NOT respond with plain text - you MUST call the StructuredOutput tool with your answer formatted according to the schema.`
|
||||
|
||||
const log = Log.create({ service: "session.prompt" })
|
||||
const elog = EffectLogger.create({ service: "session.prompt" })
|
||||
|
||||
function isOrphanedInterruptedTool(part: MessageV2.ToolPart) {
|
||||
// cleanup() marks abandoned tool_use blocks this way after retries/aborts.
|
||||
// They are not pending work and must not trigger an assistant-prefill request.
|
||||
@@ -138,7 +132,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) {
|
||||
yield* elog.info("cancel", { sessionID })
|
||||
yield* Effect.logInfo("cancel").pipe(Effect.annotateLogs({ service: "session.prompt", ...{ sessionID } }))
|
||||
yield* state.cancel(sessionID)
|
||||
})
|
||||
|
||||
@@ -297,7 +291,13 @@ export const layer = Layer.effect(
|
||||
const t = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
|
||||
yield* sessions
|
||||
.setTitle({ sessionID: input.session.id, title: t })
|
||||
.pipe(Effect.catchCause((cause) => elog.error("failed to generate title", { error: Cause.squash(cause) })))
|
||||
.pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.logError("failed to generate title").pipe(
|
||||
Effect.annotateLogs({ service: "session.prompt", ...{ error: Cause.squash(cause) } }),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
const handleSubtask = Effect.fn("SessionPrompt.handleSubtask")(function* (input: {
|
||||
@@ -399,7 +399,6 @@ export const layer = Layer.effect(
|
||||
Effect.catchCause((cause) => {
|
||||
const defect = Cause.squash(cause)
|
||||
error = defect instanceof Error ? defect : new Error(String(defect))
|
||||
log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
|
||||
return Effect.void
|
||||
}),
|
||||
Effect.onInterrupt(() =>
|
||||
@@ -795,7 +794,9 @@ export const layer = Layer.effect(
|
||||
if (part.type === "file") {
|
||||
if (part.source?.type === "resource") {
|
||||
const { clientName, uri } = part.source
|
||||
log.info("mcp resource", { clientName, uri, mime: part.mime })
|
||||
yield* Effect.logInfo("mcp resource").pipe(
|
||||
Effect.annotateLogs({ service: "session.prompt", ...{ clientName, uri, mime: part.mime } }),
|
||||
)
|
||||
const pieces: Draft<MessageV2.Part>[] = [
|
||||
{
|
||||
messageID: info.id,
|
||||
@@ -833,7 +834,9 @@ export const layer = Layer.effect(
|
||||
pieces.push({ ...part, messageID: info.id, sessionID: input.sessionID })
|
||||
} else {
|
||||
const error = Cause.squash(exit.cause)
|
||||
log.error("failed to read MCP resource", { error, clientName, uri })
|
||||
yield* Effect.logError("failed to read MCP resource").pipe(
|
||||
Effect.annotateLogs({ service: "session.prompt", ...{ error, clientName, uri } }),
|
||||
)
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
pieces.push({
|
||||
messageID: info.id,
|
||||
@@ -869,7 +872,9 @@ export const layer = Layer.effect(
|
||||
}
|
||||
break
|
||||
case "file:": {
|
||||
log.info("file", { mime: part.mime })
|
||||
yield* Effect.logInfo("file").pipe(
|
||||
Effect.annotateLogs({ service: "session.prompt", ...{ mime: part.mime } }),
|
||||
)
|
||||
const filepath = fileURLToPath(part.url)
|
||||
const referenceContext = yield* referenceContextFromFilePart(part, filepath)
|
||||
const mime = (yield* fsys.isDir(filepath)) ? "application/x-directory" : part.mime
|
||||
@@ -956,7 +961,9 @@ export const layer = Layer.effect(
|
||||
}
|
||||
} else {
|
||||
const error = Cause.squash(exit.cause)
|
||||
log.error("failed to read file", { error })
|
||||
yield* Effect.logError("failed to read file").pipe(
|
||||
Effect.annotateLogs({ service: "session.prompt", ...{ error } }),
|
||||
)
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
yield* bus.publish(Session.Event.Error, {
|
||||
sessionID: input.sessionID,
|
||||
@@ -978,7 +985,9 @@ export const layer = Layer.effect(
|
||||
const exit = yield* execRead(args).pipe(Effect.exit)
|
||||
if (Exit.isFailure(exit)) {
|
||||
const error = Cause.squash(exit.cause)
|
||||
log.error("failed to read directory", { error })
|
||||
yield* Effect.logError("failed to read directory").pipe(
|
||||
Effect.annotateLogs({ service: "session.prompt", ...{ error } }),
|
||||
)
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
yield* bus.publish(Session.Event.Error, {
|
||||
sessionID: input.sessionID,
|
||||
@@ -1095,26 +1104,22 @@ export const layer = Layer.effect(
|
||||
|
||||
const parsed = decodeMessageInfo(info, { errors: "all", propertyOrder: "original" })
|
||||
if (Exit.isFailure(parsed)) {
|
||||
log.error("invalid user message before save", {
|
||||
sessionID: input.sessionID,
|
||||
messageID: info.id,
|
||||
agent: info.agent,
|
||||
model: info.model,
|
||||
cause: Cause.pretty(parsed.cause),
|
||||
})
|
||||
yield* Effect.logError("invalid user message before save").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "session.prompt",
|
||||
...{
|
||||
sessionID: input.sessionID,
|
||||
messageID: info.id,
|
||||
agent: info.agent,
|
||||
model: info.model,
|
||||
cause: Cause.pretty(parsed.cause),
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
parts.forEach((part, index) => {
|
||||
const p = decodeMessagePart(part, { errors: "all", propertyOrder: "original" })
|
||||
if (Exit.isSuccess(p)) return
|
||||
log.error("invalid user part before save", {
|
||||
sessionID: input.sessionID,
|
||||
messageID: info.id,
|
||||
partID: part.id,
|
||||
partType: part.type,
|
||||
index,
|
||||
cause: Cause.pretty(p.cause),
|
||||
part,
|
||||
})
|
||||
})
|
||||
|
||||
yield* sessions.updateMessage(info)
|
||||
@@ -1244,14 +1249,13 @@ export const layer = Layer.effect(
|
||||
const runLoop: (sessionID: SessionID) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.run")(
|
||||
function* (sessionID: SessionID) {
|
||||
const ctx = yield* InstanceState.context
|
||||
const slog = elog.with({ sessionID })
|
||||
let structured: unknown
|
||||
let step = 0
|
||||
const session = yield* sessions.get(sessionID).pipe(Effect.orDie)
|
||||
|
||||
while (true) {
|
||||
yield* status.set(sessionID, { type: "busy" })
|
||||
yield* slog.info("loop", { step })
|
||||
yield* Effect.logInfo("loop").pipe(Effect.annotateLogs({ service: "session.prompt", sessionID, step }))
|
||||
|
||||
let msgs = yield* MessageV2.filterCompactedEffect(sessionID)
|
||||
|
||||
@@ -1280,13 +1284,17 @@ export const layer = Layer.effect(
|
||||
(part): part is MessageV2.ToolPart => part.type === "tool" && isOrphanedInterruptedTool(part),
|
||||
)
|
||||
if (orphan) {
|
||||
yield* slog.warn("loop exit with orphaned interrupted tool", {
|
||||
messageID: lastAssistant.id,
|
||||
tool: orphan.tool,
|
||||
callID: orphan.callID,
|
||||
})
|
||||
yield* Effect.logWarning("loop exit with orphaned interrupted tool").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "session.prompt",
|
||||
sessionID,
|
||||
messageID: lastAssistant.id,
|
||||
tool: orphan.tool,
|
||||
callID: orphan.callID,
|
||||
}),
|
||||
)
|
||||
}
|
||||
yield* slog.info("exiting loop")
|
||||
yield* Effect.logInfo("exiting loop").pipe(Effect.annotateLogs({ service: "session.prompt", sessionID }))
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1511,7 +1519,12 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const command = Effect.fn("SessionPrompt.command")(function* (input: CommandInput) {
|
||||
yield* elog.info("command", { sessionID: input.sessionID, command: input.command, agent: input.agent })
|
||||
yield* Effect.logInfo("command").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "session.prompt",
|
||||
...{ sessionID: input.sessionID, command: input.command, agent: input.agent },
|
||||
}),
|
||||
)
|
||||
const cmd = yield* commands.get(input.command)
|
||||
if (!cmd) {
|
||||
const available = (yield* commands.list()).map((c) => c.name)
|
||||
|
||||
@@ -3,15 +3,11 @@ import { Bus } from "../bus"
|
||||
import { Snapshot } from "../snapshot"
|
||||
import { Storage } from "@/storage/storage"
|
||||
import { SyncEvent } from "../sync"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import * as Session from "./session"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { SessionID, MessageID, PartID } from "./schema"
|
||||
import { SessionRunState } from "./run-state"
|
||||
import { SessionSummary } from "./summary"
|
||||
|
||||
const log = Log.create({ service: "session.revert" })
|
||||
|
||||
export const RevertInput = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
messageID: MessageID,
|
||||
@@ -91,7 +87,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const unrevert = Effect.fn("SessionRevert.unrevert")(function* (input: { sessionID: SessionID }) {
|
||||
log.info("unreverting", input)
|
||||
yield* Effect.logInfo("unreverting").pipe(Effect.annotateLogs({ service: "session.revert", ...input }))
|
||||
yield* state.assertNotBusy(input.sessionID)
|
||||
const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
|
||||
if (!session.revert) return session
|
||||
|
||||
@@ -24,7 +24,6 @@ import type { SQL } from "drizzle-orm"
|
||||
import { PartTable, SessionTable } from "./session.sql"
|
||||
import { ProjectTable } from "../project/project.sql"
|
||||
import { Storage } from "@/storage/storage"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import type { InstanceContext } from "../project/instance-context"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
@@ -40,9 +39,6 @@ import { Global } from "@opencode-ai/core/global"
|
||||
import { Effect, Layer, Option, Context, Schema, Types } from "effect"
|
||||
import { NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
|
||||
const log = Log.create({ service: "session" })
|
||||
|
||||
const parentTitlePrefix = "New session - "
|
||||
const childTitlePrefix = "Child session - "
|
||||
|
||||
@@ -552,7 +548,7 @@ export const layer: Layer.Layer<
|
||||
updated: Date.now(),
|
||||
},
|
||||
}
|
||||
log.info("created", result)
|
||||
yield* Effect.logInfo("created").pipe(Effect.annotateLogs({ service: "session", ...result }))
|
||||
|
||||
yield* sync.run(Event.Created, { sessionID: result.id, info: result })
|
||||
|
||||
@@ -611,7 +607,7 @@ export const layer: Layer.Layer<
|
||||
yield* sync.run(Event.Deleted, { sessionID, info: session }, { publish: hasInstance })
|
||||
yield* sync.remove(sessionID)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
yield* Effect.logError(e).pipe(Effect.annotateLogs({ service: "session" }))
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -16,11 +16,7 @@ import { MessageV2 } from "./message-v2"
|
||||
import * as Session from "./session"
|
||||
import { SessionProcessor } from "./processor"
|
||||
import { PartID } from "./schema"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
|
||||
const log = Log.create({ service: "session.tools" })
|
||||
|
||||
export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
||||
agent: Agent.Info
|
||||
model: Provider.Model
|
||||
@@ -30,7 +26,6 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
||||
messages: MessageV2.WithParts[]
|
||||
promptOps: TaskPromptOps
|
||||
}) {
|
||||
using _ = log.time("resolveTools")
|
||||
const tools: Record<string, AITool> = {}
|
||||
const run = yield* EffectBridge.make()
|
||||
const plugin = yield* Plugin.Service
|
||||
|
||||
@@ -13,10 +13,7 @@ import type { SessionID } from "@/session/schema"
|
||||
import { Database } from "@/storage/db"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Config } from "@/config/config"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { SessionShareTable } from "./share.sql"
|
||||
|
||||
const log = Log.create({ service: "share-next" })
|
||||
const disabled = process.env["OPENCODE_DISABLE_SHARE"] === "true" || process.env["OPENCODE_DISABLE_SHARE"] === "1"
|
||||
|
||||
export type Api = {
|
||||
@@ -139,11 +136,7 @@ export const layer = Layer.effect(
|
||||
s.queue.set(sessionID, next)
|
||||
yield* flush(sessionID).pipe(
|
||||
Effect.delay(1000),
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.sync(() => {
|
||||
log.error("share flush failed", { sessionID, cause })
|
||||
}),
|
||||
),
|
||||
Effect.catchCause((cause) => Effect.sync(() => {})),
|
||||
Effect.forkIn(s.scope),
|
||||
)
|
||||
})
|
||||
@@ -173,15 +166,7 @@ export const layer = Layer.effect(
|
||||
bus.subscribe(def as never).pipe(
|
||||
Effect.flatMap((stream) =>
|
||||
stream.pipe(
|
||||
Stream.runForEach((evt) =>
|
||||
fn(evt).pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.sync(() => {
|
||||
log.error("share subscriber failed", { type: def.type, cause })
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
Stream.runForEach((evt) => fn(evt).pipe(Effect.catchCause((cause) => Effect.sync(() => {})))),
|
||||
Effect.forkScoped,
|
||||
),
|
||||
),
|
||||
@@ -271,12 +256,14 @@ export const layer = Layer.effect(
|
||||
)
|
||||
|
||||
if (res.status >= 400) {
|
||||
log.warn("failed to sync share", { sessionID, shareID: share.id, status: res.status })
|
||||
yield* Effect.logWarning("failed to sync share").pipe(
|
||||
Effect.annotateLogs({ service: "share-next", ...{ sessionID, shareID: share.id, status: res.status } }),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const full = Effect.fn("ShareNext.full")(function* (sessionID: SessionID) {
|
||||
log.info("full sync", { sessionID })
|
||||
yield* Effect.logInfo("full sync").pipe(Effect.annotateLogs({ service: "share-next", ...{ sessionID } }))
|
||||
const info = yield* session.get(sessionID)
|
||||
const diffs = yield* session.diff(sessionID)
|
||||
const messages = yield* session.messages({ sessionID })
|
||||
@@ -313,7 +300,7 @@ export const layer = Layer.effect(
|
||||
|
||||
const create = Effect.fn("ShareNext.create")(function* (sessionID: SessionID) {
|
||||
if (disabled) return { id: "", url: "", secret: "" }
|
||||
log.info("creating share", { sessionID })
|
||||
yield* Effect.logInfo("creating share").pipe(Effect.annotateLogs({ service: "share-next", ...{ sessionID } }))
|
||||
const req = yield* request()
|
||||
const result = yield* HttpClientRequest.post(`${req.baseUrl}${req.api.create}`).pipe(
|
||||
HttpClientRequest.setHeaders(req.headers),
|
||||
@@ -334,11 +321,7 @@ export const layer = Layer.effect(
|
||||
const s = yield* InstanceState.get(state)
|
||||
s.shared.set(sessionID, result)
|
||||
yield* full(sessionID).pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.sync(() => {
|
||||
log.error("share full sync failed", { sessionID, cause })
|
||||
}),
|
||||
),
|
||||
Effect.catchCause((cause) => Effect.sync(() => {})),
|
||||
Effect.forkIn(s.scope),
|
||||
)
|
||||
return result
|
||||
@@ -346,7 +329,7 @@ export const layer = Layer.effect(
|
||||
|
||||
const remove = Effect.fn("ShareNext.remove")(function* (sessionID: SessionID) {
|
||||
if (disabled) return
|
||||
log.info("removing share", { sessionID })
|
||||
yield* Effect.logInfo("removing share").pipe(Effect.annotateLogs({ service: "share-next", ...{ sessionID } }))
|
||||
const s = yield* InstanceState.get(state)
|
||||
const share = yield* getCached(sessionID)
|
||||
if (!share) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } fr
|
||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
const skillConcurrency = 4
|
||||
const fileConcurrency = 8
|
||||
@@ -28,7 +27,6 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Path.Pat
|
||||
Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const log = Log.create({ service: "skill-discovery" })
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const path = yield* Path.Path
|
||||
const http = HttpClient.filterStatusOk(withTransientReadRetry(yield* HttpClient.HttpClient))
|
||||
@@ -44,7 +42,6 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Path.Pat
|
||||
Effect.as(true),
|
||||
Effect.catch((err) =>
|
||||
Effect.sync(() => {
|
||||
log.error("failed to download", { url, err })
|
||||
return false
|
||||
}),
|
||||
),
|
||||
@@ -56,7 +53,9 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Path.Pat
|
||||
const index = new URL("index.json", base).href
|
||||
const host = base.slice(0, -1)
|
||||
|
||||
log.info("fetching index", { url: index })
|
||||
yield* Effect.logInfo("fetching index").pipe(
|
||||
Effect.annotateLogs({ service: "skill-discovery", ...{ url: index } }),
|
||||
)
|
||||
|
||||
const data = yield* HttpClientRequest.get(index).pipe(
|
||||
HttpClientRequest.acceptJson,
|
||||
@@ -64,7 +63,6 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Path.Pat
|
||||
Effect.flatMap(HttpClientResponse.schemaBodyJson(Index)),
|
||||
Effect.catch((err) =>
|
||||
Effect.sync(() => {
|
||||
log.error("failed to fetch index", { url: index, err })
|
||||
return null
|
||||
}),
|
||||
),
|
||||
@@ -74,7 +72,6 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Path.Pat
|
||||
|
||||
const list = data.skills.filter((skill) => {
|
||||
if (!skill.files.includes("SKILL.md")) {
|
||||
log.warn("skill entry missing SKILL.md", { url: index, skill: skill.name })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -12,12 +12,9 @@ import { Config } from "@/config/config"
|
||||
import { ConfigMarkdown } from "@/config/markdown"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { Glob } from "@opencode-ai/core/util/glob"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Discovery } from "./discovery"
|
||||
import CUSTOMIZE_OPENCODE_SKILL_BODY from "./prompt/customize-opencode.md" with { type: "text" }
|
||||
import { isRecord } from "@/util/record"
|
||||
|
||||
const log = Log.create({ service: "skill" })
|
||||
const CLAUDE_EXTERNAL_DIR = ".claude"
|
||||
const AGENTS_EXTERNAL_DIR = ".agents"
|
||||
const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md"
|
||||
@@ -113,7 +110,9 @@ const add = Effect.fnUntraced(function* (state: State, match: string, bus: Bus.I
|
||||
: `Failed to parse skill ${match}`
|
||||
const { Session } = yield* Effect.promise(() => import("@/session/session"))
|
||||
yield* bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
|
||||
log.error("failed to load skill", { skill: match, err })
|
||||
yield* Effect.logError("failed to load skill").pipe(
|
||||
Effect.annotateLogs({ service: "skill", ...{ skill: match, err } }),
|
||||
)
|
||||
return undefined
|
||||
}),
|
||||
),
|
||||
@@ -124,11 +123,16 @@ const add = Effect.fnUntraced(function* (state: State, match: string, bus: Bus.I
|
||||
if (!isSkillFrontmatter(md.data)) return
|
||||
|
||||
if (state.skills[md.data.name]) {
|
||||
log.warn("duplicate skill name", {
|
||||
name: md.data.name,
|
||||
existing: state.skills[md.data.name].location,
|
||||
duplicate: match,
|
||||
})
|
||||
yield* Effect.logWarning("duplicate skill name").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "skill",
|
||||
...{
|
||||
name: md.data.name,
|
||||
existing: state.skills[md.data.name].location,
|
||||
duplicate: match,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
state.dirs.add(path.dirname(match))
|
||||
@@ -159,7 +163,6 @@ const scan = Effect.fnUntraced(function* (
|
||||
}).pipe(
|
||||
Effect.catch((error) => {
|
||||
if (!opts?.scope) return Effect.die(error)
|
||||
log.error(`failed to scan ${opts.scope} skills`, { dir: root, error })
|
||||
return Effect.succeed([] as string[])
|
||||
}),
|
||||
)
|
||||
@@ -212,7 +215,7 @@ const discoverSkills = Effect.fnUntraced(function* (
|
||||
const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item
|
||||
const dir = path.isAbsolute(expanded) ? expanded : path.join(directory, expanded)
|
||||
if (!(yield* fsys.isDir(dir))) {
|
||||
log.warn("skill path not found", { path: dir })
|
||||
yield* Effect.logWarning("skill path not found").pipe(Effect.annotateLogs({ service: "skill", ...{ path: dir } }))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -238,7 +241,9 @@ const loadSkills = Effect.fnUntraced(function* (state: State, discovered: Discov
|
||||
discard: true,
|
||||
})
|
||||
|
||||
log.info("init", { count: Object.keys(state.skills).length })
|
||||
yield* Effect.logInfo("init").pipe(
|
||||
Effect.annotateLogs({ service: "skill", ...{ count: Object.keys(state.skills).length } }),
|
||||
)
|
||||
})
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Skill") {}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Hash } from "@opencode-ai/core/util/hash"
|
||||
import { Config } from "@/config/config"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
export const Patch = Schema.Struct({
|
||||
hash: Schema.String,
|
||||
@@ -27,8 +26,6 @@ export const FileDiff = Schema.Struct({
|
||||
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
|
||||
}).annotate({ identifier: "SnapshotFileDiff" })
|
||||
export type FileDiff = typeof FileDiff.Type
|
||||
|
||||
const log = Log.create({ service: "snapshot" })
|
||||
const prune = "7.days"
|
||||
const limit = 2 * 1024 * 1024
|
||||
const core = ["-c", "core.longpaths=true", "-c", "core.symlinks=true"]
|
||||
@@ -154,10 +151,15 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
},
|
||||
)
|
||||
if (result.code === 0) return
|
||||
log.warn("failed to add snapshot files", {
|
||||
exitCode: result.code,
|
||||
stderr: result.stderr,
|
||||
})
|
||||
yield* Effect.logWarning("failed to add snapshot files").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "snapshot",
|
||||
...{
|
||||
exitCode: result.code,
|
||||
stderr: result.stderr,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const exists = (file: string) => fs.exists(file).pipe(Effect.orDie)
|
||||
@@ -207,12 +209,17 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
{ concurrency: 2 },
|
||||
)
|
||||
if (diff.code !== 0 || other.code !== 0) {
|
||||
log.warn("failed to list snapshot files", {
|
||||
diffCode: diff.code,
|
||||
diffStderr: diff.stderr,
|
||||
otherCode: other.code,
|
||||
otherStderr: other.stderr,
|
||||
})
|
||||
yield* Effect.logWarning("failed to list snapshot files").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "snapshot",
|
||||
...{
|
||||
diffCode: diff.code,
|
||||
diffStderr: diff.stderr,
|
||||
otherCode: other.code,
|
||||
otherStderr: other.stderr,
|
||||
},
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -228,7 +235,9 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
// Remove newly-ignored files from snapshot index to prevent re-adding
|
||||
if (ignored.size > 0) {
|
||||
const ignoredFiles = Array.from(ignored)
|
||||
log.info("removing gitignored files from snapshot", { count: ignoredFiles.length })
|
||||
yield* Effect.logInfo("removing gitignored files from snapshot").pipe(
|
||||
Effect.annotateLogs({ service: "snapshot", ...{ count: ignoredFiles.length } }),
|
||||
)
|
||||
yield* drop(ignoredFiles)
|
||||
}
|
||||
|
||||
@@ -265,13 +274,18 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
if (!(yield* exists(state.gitdir))) return
|
||||
const result = yield* git(args(["gc", `--prune=${prune}`]), { cwd: state.directory })
|
||||
if (result.code !== 0) {
|
||||
log.warn("cleanup failed", {
|
||||
exitCode: result.code,
|
||||
stderr: result.stderr,
|
||||
})
|
||||
yield* Effect.logWarning("cleanup failed").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "snapshot",
|
||||
...{
|
||||
exitCode: result.code,
|
||||
stderr: result.stderr,
|
||||
},
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
log.info("cleanup", { prune })
|
||||
yield* Effect.logInfo("cleanup").pipe(Effect.annotateLogs({ service: "snapshot", ...{ prune } }))
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -290,12 +304,14 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
yield* git(["--git-dir", state.gitdir, "config", "core.longpaths", "true"])
|
||||
yield* git(["--git-dir", state.gitdir, "config", "core.symlinks", "true"])
|
||||
yield* git(["--git-dir", state.gitdir, "config", "core.fsmonitor", "false"])
|
||||
log.info("initialized")
|
||||
yield* Effect.logInfo("initialized").pipe(Effect.annotateLogs({ service: "snapshot" }))
|
||||
}
|
||||
yield* add()
|
||||
const result = yield* git(args(["write-tree"]), { cwd: state.directory })
|
||||
const hash = result.text.trim()
|
||||
log.info("tracking", { hash, cwd: state.directory, git: state.gitdir })
|
||||
yield* Effect.logInfo("tracking").pipe(
|
||||
Effect.annotateLogs({ service: "snapshot", ...{ hash, cwd: state.directory, git: state.gitdir } }),
|
||||
)
|
||||
return hash
|
||||
}),
|
||||
)
|
||||
@@ -312,7 +328,9 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
},
|
||||
)
|
||||
if (result.code !== 0) {
|
||||
log.warn("failed to get diff", { hash, exitCode: result.code })
|
||||
yield* Effect.logWarning("failed to get diff").pipe(
|
||||
Effect.annotateLogs({ service: "snapshot", ...{ hash, exitCode: result.code } }),
|
||||
)
|
||||
return { hash, files: [] }
|
||||
}
|
||||
const files = result.text
|
||||
@@ -337,25 +355,37 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
const restore = Effect.fnUntraced(function* (snapshot: string) {
|
||||
return yield* locked(
|
||||
Effect.gen(function* () {
|
||||
log.info("restore", { commit: snapshot })
|
||||
yield* Effect.logInfo("restore").pipe(
|
||||
Effect.annotateLogs({ service: "snapshot", ...{ commit: snapshot } }),
|
||||
)
|
||||
const result = yield* git([...core, ...args(["read-tree", snapshot])], { cwd: state.worktree })
|
||||
if (result.code === 0) {
|
||||
const checkout = yield* git([...core, ...args(["checkout-index", "-a", "-f"])], {
|
||||
cwd: state.worktree,
|
||||
})
|
||||
if (checkout.code === 0) return
|
||||
log.error("failed to restore snapshot", {
|
||||
snapshot,
|
||||
exitCode: checkout.code,
|
||||
stderr: checkout.stderr,
|
||||
})
|
||||
yield* Effect.logError("failed to restore snapshot").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "snapshot",
|
||||
...{
|
||||
snapshot,
|
||||
exitCode: checkout.code,
|
||||
stderr: checkout.stderr,
|
||||
},
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
log.error("failed to restore snapshot", {
|
||||
snapshot,
|
||||
exitCode: result.code,
|
||||
stderr: result.stderr,
|
||||
})
|
||||
yield* Effect.logError("failed to restore snapshot").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "snapshot",
|
||||
...{
|
||||
snapshot,
|
||||
exitCode: result.code,
|
||||
stderr: result.stderr,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -378,7 +408,9 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
}
|
||||
|
||||
const single = Effect.fnUntraced(function* (op: (typeof ops)[number]) {
|
||||
log.info("reverting", { file: op.file, hash: op.hash })
|
||||
yield* Effect.logInfo("reverting").pipe(
|
||||
Effect.annotateLogs({ service: "snapshot", ...{ file: op.file, hash: op.hash } }),
|
||||
)
|
||||
const result = yield* git([...core, ...args(["checkout", op.hash, "--", op.file])], {
|
||||
cwd: state.worktree,
|
||||
})
|
||||
@@ -387,10 +419,14 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
cwd: state.worktree,
|
||||
})
|
||||
if (tree.code === 0 && tree.text.trim()) {
|
||||
log.info("file existed in snapshot but checkout failed, keeping", { file: op.file, hash: op.hash })
|
||||
yield* Effect.logInfo("file existed in snapshot but checkout failed, keeping").pipe(
|
||||
Effect.annotateLogs({ service: "snapshot", ...{ file: op.file, hash: op.hash } }),
|
||||
)
|
||||
return
|
||||
}
|
||||
log.info("file did not exist in snapshot, deleting", { file: op.file, hash: op.hash })
|
||||
yield* Effect.logInfo("file did not exist in snapshot, deleting").pipe(
|
||||
Effect.annotateLogs({ service: "snapshot", ...{ file: op.file, hash: op.hash } }),
|
||||
)
|
||||
yield* remove(op.file)
|
||||
})
|
||||
|
||||
@@ -423,10 +459,15 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
)
|
||||
|
||||
if (tree.code !== 0) {
|
||||
log.info("batched ls-tree failed, falling back to single-file revert", {
|
||||
hash: first.hash,
|
||||
files: run.length,
|
||||
})
|
||||
yield* Effect.logInfo("batched ls-tree failed, falling back to single-file revert").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "snapshot",
|
||||
...{
|
||||
hash: first.hash,
|
||||
files: run.length,
|
||||
},
|
||||
}),
|
||||
)
|
||||
for (const op of run) {
|
||||
yield* single(op)
|
||||
}
|
||||
@@ -443,7 +484,9 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
)
|
||||
const list = run.filter((item) => have.has(item.rel))
|
||||
if (list.length) {
|
||||
log.info("reverting", { hash: first.hash, files: list.length })
|
||||
yield* Effect.logInfo("reverting").pipe(
|
||||
Effect.annotateLogs({ service: "snapshot", ...{ hash: first.hash, files: list.length } }),
|
||||
)
|
||||
const result = yield* git(
|
||||
[...core, ...args(["checkout", first.hash, "--", ...list.map((item) => item.file)])],
|
||||
{
|
||||
@@ -451,10 +494,15 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
},
|
||||
)
|
||||
if (result.code !== 0) {
|
||||
log.info("batched checkout failed, falling back to single-file revert", {
|
||||
hash: first.hash,
|
||||
files: list.length,
|
||||
})
|
||||
yield* Effect.logInfo("batched checkout failed, falling back to single-file revert").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "snapshot",
|
||||
...{
|
||||
hash: first.hash,
|
||||
files: list.length,
|
||||
},
|
||||
}),
|
||||
)
|
||||
for (const op of run) {
|
||||
yield* single(op)
|
||||
}
|
||||
@@ -465,7 +513,9 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
|
||||
for (const op of run) {
|
||||
if (have.has(op.rel)) continue
|
||||
log.info("file did not exist in snapshot, deleting", { file: op.file, hash: op.hash })
|
||||
yield* Effect.logInfo("file did not exist in snapshot, deleting").pipe(
|
||||
Effect.annotateLogs({ service: "snapshot", ...{ file: op.file, hash: op.hash } }),
|
||||
)
|
||||
yield* remove(op.file)
|
||||
}
|
||||
|
||||
@@ -483,11 +533,16 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
cwd: state.worktree,
|
||||
})
|
||||
if (result.code !== 0) {
|
||||
log.warn("failed to get diff", {
|
||||
hash,
|
||||
exitCode: result.code,
|
||||
stderr: result.stderr,
|
||||
})
|
||||
yield* Effect.logWarning("failed to get diff").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "snapshot",
|
||||
...{
|
||||
hash,
|
||||
exitCode: result.code,
|
||||
stderr: result.stderr,
|
||||
},
|
||||
}),
|
||||
)
|
||||
return ""
|
||||
}
|
||||
return result.text.trim()
|
||||
@@ -563,16 +618,22 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
{ stdin: refs.map((item) => item.ref).join("\n") + "\n" },
|
||||
)
|
||||
if (batch.exitCode !== 0) {
|
||||
log.info("git cat-file --batch failed during snapshot diff, falling back to per-file git show", {
|
||||
stderr: batch.stderr.toString("utf8"),
|
||||
refs: refs.length,
|
||||
})
|
||||
yield* Effect.logInfo(
|
||||
"git cat-file --batch failed during snapshot diff, falling back to per-file git show",
|
||||
).pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "snapshot",
|
||||
...{
|
||||
stderr: batch.stderr.toString("utf8"),
|
||||
refs: refs.length,
|
||||
},
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
const out = batch.stdout
|
||||
|
||||
const fail = (msg: string, extra?: Record<string, string>) => {
|
||||
log.info(msg, { ...extra, refs: refs.length })
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -712,7 +773,6 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
|
||||
yield* cleanup().pipe(
|
||||
Effect.catchCause((cause) => {
|
||||
log.error("cleanup loop failed", { cause: Cause.pretty(cause) })
|
||||
return Effect.void
|
||||
}),
|
||||
Effect.repeat(Schedule.spaced(Duration.hours(1))),
|
||||
|
||||
@@ -5,7 +5,6 @@ export * from "drizzle-orm"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { LocalContext } from "@/util/local-context"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import path from "path"
|
||||
import { readFileSync, readdirSync, existsSync } from "fs"
|
||||
@@ -20,9 +19,6 @@ declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number; name: strin
|
||||
export const NotFoundError = NamedError.create("NotFoundError", {
|
||||
message: Schema.String,
|
||||
})
|
||||
|
||||
const log = Log.create({ service: "db" })
|
||||
|
||||
type DatabaseFlags = Pick<RuntimeFlags.Info, "disableChannelDb" | "skipMigrations">
|
||||
|
||||
const readRuntimeFlags = () =>
|
||||
@@ -97,8 +93,6 @@ export const Client = Object.assign(
|
||||
if (loaded) return client as Client
|
||||
|
||||
const dbPath = getPath(flags)
|
||||
log.info("opening database", { path: dbPath })
|
||||
|
||||
const db = init(dbPath)
|
||||
|
||||
db.run("PRAGMA journal_mode = WAL")
|
||||
@@ -114,10 +108,6 @@ export const Client = Object.assign(
|
||||
? OPENCODE_MIGRATIONS
|
||||
: migrations(path.join(import.meta.dirname, "../../migration"))
|
||||
if (entries.length > 0) {
|
||||
log.info("applying migrations", {
|
||||
count: entries.length,
|
||||
mode: typeof OPENCODE_MIGRATIONS !== "undefined" ? "bundled" : "dev",
|
||||
})
|
||||
if (flags.skipMigrations) {
|
||||
for (const item of entries) {
|
||||
item.sql = "select 1;"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { SQLiteBunDatabase } from "drizzle-orm/bun-sqlite"
|
||||
import type { NodeSQLiteDatabase } from "drizzle-orm/node-sqlite"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { ProjectTable } from "../project/project.sql"
|
||||
import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../session/session.sql"
|
||||
import { SessionShareTable } from "../share/share.sql"
|
||||
@@ -9,9 +8,6 @@ import path from "path"
|
||||
import { existsSync } from "fs"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Glob } from "@opencode-ai/core/util/glob"
|
||||
|
||||
const log = Log.create({ service: "json-migration" })
|
||||
|
||||
export type Progress = {
|
||||
current: number
|
||||
total: number
|
||||
@@ -26,7 +22,6 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
|
||||
const storageDir = path.join(Global.Path.data, "storage")
|
||||
|
||||
if (!existsSync(storageDir)) {
|
||||
log.info("storage directory does not exist, skipping migration")
|
||||
return {
|
||||
projects: 0,
|
||||
sessions: 0,
|
||||
@@ -38,8 +33,6 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
|
||||
errors: [] as string[],
|
||||
}
|
||||
}
|
||||
|
||||
log.info("starting json to sqlite migration", { storageDir })
|
||||
const start = performance.now()
|
||||
|
||||
// const db = drizzle({ client: sqlite })
|
||||
@@ -105,9 +98,6 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-scan all files upfront to avoid repeated glob operations
|
||||
log.info("scanning files...")
|
||||
const [projectFiles, sessionFiles, messageFiles, partFiles, todoFiles, permFiles, shareFiles] = await Promise.all([
|
||||
list("project/*.json"),
|
||||
list("session/*/*.json"),
|
||||
@@ -117,17 +107,6 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
|
||||
list("permission/*.json"),
|
||||
list("session_share/*.json"),
|
||||
])
|
||||
|
||||
log.info("file scan complete", {
|
||||
projects: projectFiles.length,
|
||||
sessions: sessionFiles.length,
|
||||
messages: messageFiles.length,
|
||||
parts: partFiles.length,
|
||||
todos: todoFiles.length,
|
||||
permissions: permFiles.length,
|
||||
shares: shareFiles.length,
|
||||
})
|
||||
|
||||
const total = Math.max(
|
||||
1,
|
||||
projectFiles.length +
|
||||
@@ -179,10 +158,7 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
|
||||
}
|
||||
stats.projects += insert(projectValues, ProjectTable, "project")
|
||||
step("projects", end - i)
|
||||
}
|
||||
log.info("migrated projects", { count: stats.projects, duration: Math.round(performance.now() - start) })
|
||||
|
||||
// Migrate sessions (depends on projects)
|
||||
} // Migrate sessions (depends on projects)
|
||||
// Derive all IDs from directory/file paths, not JSON content, since earlier
|
||||
// migrations may have moved sessions to new directories without updating the JSON
|
||||
const sessionProjects = sessionFiles.map((file) => path.basename(path.dirname(file)))
|
||||
@@ -233,9 +209,7 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
|
||||
stats.sessions += insert(sessionValues, SessionTable, "session")
|
||||
step("sessions", end - i)
|
||||
}
|
||||
log.info("migrated sessions", { count: stats.sessions })
|
||||
if (orphans.sessions > 0) {
|
||||
log.warn("skipped orphaned sessions", { count: orphans.sessions })
|
||||
}
|
||||
|
||||
// Migrate messages using pre-scanned file map
|
||||
@@ -276,10 +250,7 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
|
||||
values.length = count
|
||||
stats.messages += insert(values, MessageTable, "message")
|
||||
step("messages", end - i)
|
||||
}
|
||||
log.info("migrated messages", { count: stats.messages })
|
||||
|
||||
// Migrate parts using pre-scanned file map
|
||||
} // Migrate parts using pre-scanned file map
|
||||
for (let i = 0; i < partFiles.length; i += batchSize) {
|
||||
const end = Math.min(i + batchSize, partFiles.length)
|
||||
const batch = await read(partFiles, i, end)
|
||||
@@ -314,10 +285,7 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
|
||||
values.length = count
|
||||
stats.parts += insert(values, PartTable, "part")
|
||||
step("parts", end - i)
|
||||
}
|
||||
log.info("migrated parts", { count: stats.parts })
|
||||
|
||||
// Migrate todos
|
||||
} // Migrate todos
|
||||
const todoSessions = todoFiles.map((file) => path.basename(file, ".json"))
|
||||
for (let i = 0; i < todoFiles.length; i += batchSize) {
|
||||
const end = Math.min(i + batchSize, todoFiles.length)
|
||||
@@ -352,9 +320,7 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
|
||||
stats.todos += insert(values, TodoTable, "todo")
|
||||
step("todos", end - i)
|
||||
}
|
||||
log.info("migrated todos", { count: stats.todos })
|
||||
if (orphans.todos > 0) {
|
||||
log.warn("skipped orphaned todos", { count: orphans.todos })
|
||||
}
|
||||
|
||||
// Migrate permissions
|
||||
@@ -377,9 +343,7 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
|
||||
stats.permissions += insert(permValues, PermissionTable, "permission")
|
||||
step("permissions", end - i)
|
||||
}
|
||||
log.info("migrated permissions", { count: stats.permissions })
|
||||
if (orphans.permissions > 0) {
|
||||
log.warn("skipped orphaned permissions", { count: orphans.permissions })
|
||||
}
|
||||
|
||||
// Migrate session shares
|
||||
@@ -406,27 +370,11 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
|
||||
stats.shares += insert(shareValues, SessionShareTable, "session_share")
|
||||
step("shares", end - i)
|
||||
}
|
||||
log.info("migrated session shares", { count: stats.shares })
|
||||
if (orphans.shares > 0) {
|
||||
log.warn("skipped orphaned session shares", { count: orphans.shares })
|
||||
}
|
||||
|
||||
db.run("COMMIT")
|
||||
|
||||
log.info("json migration complete", {
|
||||
projects: stats.projects,
|
||||
sessions: stats.sessions,
|
||||
messages: stats.messages,
|
||||
parts: stats.parts,
|
||||
todos: stats.todos,
|
||||
permissions: stats.permissions,
|
||||
shares: stats.shares,
|
||||
errorCount: stats.errors.length,
|
||||
duration: Math.round(performance.now() - start),
|
||||
})
|
||||
|
||||
if (stats.errors.length > 0) {
|
||||
log.warn("migration errors", { errors: stats.errors.slice(0, 20) })
|
||||
}
|
||||
|
||||
progress?.({ current: total, total, label: "complete" })
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import path from "path"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Effect, Exit, Layer, Option, RcMap, Schema, Context, TxReentrantLock } from "effect"
|
||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||
import { Git } from "@/git"
|
||||
|
||||
const log = Log.create({ service: "storage" })
|
||||
|
||||
type Migration = (
|
||||
dir: string,
|
||||
fs: AppFileSystem.Interface,
|
||||
@@ -95,7 +91,7 @@ const MIGRATIONS: Migration[] = [
|
||||
for (const projectDir of projectDirs) {
|
||||
const full = path.join(project, projectDir)
|
||||
if (!(yield* fs.isDir(full))) continue
|
||||
log.info(`migrating project ${projectDir}`)
|
||||
yield* Effect.logInfo(`migrating project ${projectDir}`).pipe(Effect.annotateLogs({ service: "storage" }))
|
||||
let projectID = projectDir
|
||||
let worktree = "/"
|
||||
|
||||
@@ -141,43 +137,59 @@ const MIGRATIONS: Migration[] = [
|
||||
),
|
||||
)
|
||||
|
||||
log.info(`migrating sessions for project ${projectID}`)
|
||||
yield* Effect.logInfo(`migrating sessions for project ${projectID}`).pipe(
|
||||
Effect.annotateLogs({ service: "storage" }),
|
||||
)
|
||||
for (const sessionFile of yield* fs.glob("storage/session/info/*.json", {
|
||||
cwd: full,
|
||||
absolute: true,
|
||||
})) {
|
||||
const dest = path.join(dir, "session", projectID, path.basename(sessionFile))
|
||||
log.info("copying", { sessionFile, dest })
|
||||
yield* Effect.logInfo("copying").pipe(Effect.annotateLogs({ service: "storage", ...{ sessionFile, dest } }))
|
||||
const session = yield* fs.readJson(sessionFile)
|
||||
const info = decodeSession(session, { onExcessProperty: "preserve" })
|
||||
yield* fs.writeWithDirs(dest, JSON.stringify(session, null, 2))
|
||||
if (Option.isNone(info)) continue
|
||||
log.info(`migrating messages for session ${info.value.id}`)
|
||||
yield* Effect.logInfo(`migrating messages for session ${info.value.id}`).pipe(
|
||||
Effect.annotateLogs({ service: "storage" }),
|
||||
)
|
||||
for (const msgFile of yield* fs.glob(`storage/session/message/${info.value.id}/*.json`, {
|
||||
cwd: full,
|
||||
absolute: true,
|
||||
})) {
|
||||
const next = path.join(dir, "message", info.value.id, path.basename(msgFile))
|
||||
log.info("copying", {
|
||||
msgFile,
|
||||
dest: next,
|
||||
})
|
||||
yield* Effect.logInfo("copying").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "storage",
|
||||
...{
|
||||
msgFile,
|
||||
dest: next,
|
||||
},
|
||||
}),
|
||||
)
|
||||
const message = yield* fs.readJson(msgFile)
|
||||
const item = decodeMessage(message, { onExcessProperty: "preserve" })
|
||||
yield* fs.writeWithDirs(next, JSON.stringify(message, null, 2))
|
||||
if (Option.isNone(item)) continue
|
||||
|
||||
log.info(`migrating parts for message ${item.value.id}`)
|
||||
yield* Effect.logInfo(`migrating parts for message ${item.value.id}`).pipe(
|
||||
Effect.annotateLogs({ service: "storage" }),
|
||||
)
|
||||
for (const partFile of yield* fs.glob(`storage/session/part/${info.value.id}/${item.value.id}/*.json`, {
|
||||
cwd: full,
|
||||
absolute: true,
|
||||
})) {
|
||||
const out = path.join(dir, "part", item.value.id, path.basename(partFile))
|
||||
const part = yield* fs.readJson(partFile)
|
||||
log.info("copying", {
|
||||
partFile,
|
||||
dest: out,
|
||||
})
|
||||
yield* Effect.logInfo("copying").pipe(
|
||||
Effect.annotateLogs({
|
||||
service: "storage",
|
||||
...{
|
||||
partFile,
|
||||
dest: out,
|
||||
},
|
||||
}),
|
||||
)
|
||||
yield* fs.writeWithDirs(out, JSON.stringify(part, null, 2))
|
||||
}
|
||||
}
|
||||
@@ -235,11 +247,13 @@ export const layer = Layer.effect(
|
||||
Effect.orElseSucceed(() => 0),
|
||||
)
|
||||
for (let i = migration; i < MIGRATIONS.length; i++) {
|
||||
log.info("running migration", { index: i })
|
||||
yield* Effect.logInfo("running migration").pipe(Effect.annotateLogs({ service: "storage", ...{ index: i } }))
|
||||
const step = MIGRATIONS[i]!
|
||||
const exit = yield* Effect.exit(step(dir, fs, git))
|
||||
if (Exit.isFailure(exit)) {
|
||||
log.error("failed to run migration", { index: i, cause: exit.cause })
|
||||
yield* Effect.logError("failed to run migration").pipe(
|
||||
Effect.annotateLogs({ service: "storage", ...{ index: i, cause: exit.cause } }),
|
||||
)
|
||||
break
|
||||
}
|
||||
yield* fs.writeWithDirs(marker, String(i + 1))
|
||||
|
||||
@@ -2,13 +2,29 @@ import yargs from "yargs"
|
||||
import { TuiThreadCommand } from "./cli/cmd/tui/thread"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { hideBin } from "yargs/helpers"
|
||||
import { Log } from "./node"
|
||||
|
||||
Log.init({
|
||||
print: false,
|
||||
})
|
||||
const args = hideBin(process.argv)
|
||||
type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR"
|
||||
|
||||
const cli = yargs(hideBin(process.argv))
|
||||
function flag(name: string) {
|
||||
const index = args.indexOf(name)
|
||||
if (index >= 0) return args[index + 1]
|
||||
const value = args.find((arg) => arg.startsWith(name + "="))
|
||||
return value?.slice(name.length + 1)
|
||||
}
|
||||
|
||||
function parseLogLevel(value: string | undefined): LogLevel | undefined {
|
||||
if (value === "DEBUG" || value === "INFO" || value === "WARN" || value === "ERROR") return value
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (args.includes("--print-logs")) process.env.OPENCODE_PRINT_LOGS = "1"
|
||||
const logFile = flag("--log-file")
|
||||
if (logFile) process.env.OPENCODE_LOG_FILE = logFile
|
||||
const logLevel = parseLogLevel(flag("--log-level"))
|
||||
if (logLevel) process.env.OPENCODE_LOG_LEVEL = logLevel
|
||||
|
||||
const cli = yargs(args)
|
||||
.parserConfiguration({ "populate--": true })
|
||||
.scriptName("opencode")
|
||||
.wrap(100)
|
||||
@@ -20,6 +36,10 @@ const cli = yargs(hideBin(process.argv))
|
||||
describe: "print logs to stderr",
|
||||
type: "boolean",
|
||||
})
|
||||
.option("log-file", {
|
||||
describe: "path to JSONL log file",
|
||||
type: "string",
|
||||
})
|
||||
.option("log-level", {
|
||||
describe: "log level",
|
||||
type: "string",
|
||||
|
||||
@@ -25,7 +25,6 @@ import { WebSearchTool } from "./websearch"
|
||||
import { RepoCloneTool } from "./repo_clone"
|
||||
import { RepoOverviewTool } from "./repo_overview"
|
||||
import { RepositoryCache } from "@/reference/repository-cache"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { LspTool } from "./lsp"
|
||||
import * as Truncate from "./truncate"
|
||||
import { ApplyPatchTool } from "./apply_patch"
|
||||
@@ -53,9 +52,6 @@ import { Permission } from "@/permission"
|
||||
import { Reference } from "@/reference/reference"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
|
||||
const log = Log.create({ service: "tool.registry" })
|
||||
|
||||
export function webSearchEnabled(providerID: ProviderID, flags = { exa: false, parallel: false }) {
|
||||
return providerID === ProviderID.opencode || flags.exa || flags.parallel
|
||||
}
|
||||
@@ -330,7 +326,6 @@ export const layer: Layer.Layer<
|
||||
return yield* Effect.forEach(
|
||||
filtered,
|
||||
Effect.fnUntraced(function* (tool: Tool.Def) {
|
||||
using _ = log.time(tool.id)
|
||||
const output = {
|
||||
description: tool.description,
|
||||
parameters: tool.parameters,
|
||||
|
||||
@@ -3,7 +3,6 @@ import os from "os"
|
||||
import { createWriteStream } from "node:fs"
|
||||
import * as Tool from "./tool"
|
||||
import path from "path"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { containsPath, type InstanceContext } from "../project/instance-context"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { lazy } from "@/util/lazy"
|
||||
@@ -81,9 +80,6 @@ type Chunk = {
|
||||
text: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export const log = Log.create({ service: "shell-tool" })
|
||||
|
||||
const resolveWasm = (asset: string) => {
|
||||
if (asset.startsWith("file://")) return fileURLToPath(asset)
|
||||
if (asset.startsWith("/") || /^[a-z]:/i.test(asset)) return asset
|
||||
@@ -393,7 +389,9 @@ export const ShellTool = Tool.define(
|
||||
if (cmd && (FILES.has(cmd) || (shellKind === "cmd" && CMD_FILES.has(cmd)))) {
|
||||
for (const arg of pathArgs(command, ps, shellKind === "cmd")) {
|
||||
const resolved = yield* argPath(arg, cwd, ps, shell)
|
||||
log.info("resolved path", { arg, resolved })
|
||||
yield* Effect.logInfo("resolved path").pipe(
|
||||
Effect.annotateLogs({ service: "shell-tool", ...{ arg, resolved } }),
|
||||
)
|
||||
if (!resolved || containsPath(resolved, instance)) continue
|
||||
const dir = (yield* fs.isDir(resolved)) ? resolved : path.dirname(resolved)
|
||||
scan.dirs.add(dir)
|
||||
@@ -602,7 +600,9 @@ export const ShellTool = Tool.define(
|
||||
const name = Shell.name(shell)
|
||||
const limits = yield* trunc.limits()
|
||||
const prompt = ShellPrompt.render(name, process.platform, limits, defaultTimeoutMs)
|
||||
log.info("shell tool using shell", { shell })
|
||||
yield* Effect.logInfo("shell tool using shell").pipe(
|
||||
Effect.annotateLogs({ service: "shell-tool", ...{ shell } }),
|
||||
)
|
||||
|
||||
return {
|
||||
description: prompt.description,
|
||||
|
||||
@@ -6,11 +6,8 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { evaluate } from "@/permission/evaluate"
|
||||
import { Config } from "@/config/config"
|
||||
import { Identifier } from "../id/id"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { ToolID } from "./schema"
|
||||
import { TRUNCATION_DIR } from "./truncation-dir"
|
||||
|
||||
const log = Log.create({ service: "truncation" })
|
||||
const RETENTION = Duration.days(7)
|
||||
|
||||
export const MAX_LINES = 2000
|
||||
@@ -143,7 +140,6 @@ export const layer = Layer.effect(
|
||||
|
||||
yield* cleanup().pipe(
|
||||
Effect.catchCause((cause) => {
|
||||
log.error("truncation cleanup failed", { cause: Cause.pretty(cause) })
|
||||
return Effect.void
|
||||
}),
|
||||
Effect.repeat(Schedule.spaced(Duration.hours(1))),
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Database } from "@/storage/db"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { ProjectTable } from "../project/project.sql"
|
||||
import type { ProjectID } from "../project/schema"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Slug } from "@opencode-ai/core/util/slug"
|
||||
import { errorMessage } from "../util/error"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
@@ -18,9 +17,6 @@ import { NodePath } from "@effect/platform-node"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { AppProcess } from "@opencode-ai/core/process"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
|
||||
const log = Log.create({ service: "worktree" })
|
||||
|
||||
export const Event = {
|
||||
Ready: BusEvent.define(
|
||||
"worktree.ready",
|
||||
@@ -247,7 +243,9 @@ export const layer: Layer.Layer<
|
||||
const populated = yield* git(["reset", "--hard"], { cwd: info.directory })
|
||||
if (populated.code !== 0) {
|
||||
const message = populated.stderr || populated.text || "Failed to populate worktree"
|
||||
log.error("worktree checkout failed", { directory: info.directory, message })
|
||||
yield* Effect.logError("worktree checkout failed").pipe(
|
||||
Effect.annotateLogs({ service: "worktree", ...{ directory: info.directory, message } }),
|
||||
)
|
||||
GlobalBus.emit("event", {
|
||||
directory: info.directory,
|
||||
project: ctx.project.id,
|
||||
@@ -262,7 +260,6 @@ export const layer: Layer.Layer<
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
const message = errorMessage(error)
|
||||
log.error("worktree bootstrap failed", { directory: info.directory, message })
|
||||
GlobalBus.emit("event", {
|
||||
directory: info.directory,
|
||||
project: ctx.project.id,
|
||||
@@ -291,7 +288,13 @@ export const layer: Layer.Layer<
|
||||
const createFromInfo = Effect.fn("Worktree.createFromInfo")(function* (info: Info, startCommand?: string) {
|
||||
yield* setup(info)
|
||||
yield* boot(info, startCommand).pipe(
|
||||
Effect.catchCause((cause) => Effect.sync(() => log.error("worktree bootstrap failed", { cause }))),
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.sync(() =>
|
||||
Effect.logError("worktree bootstrap failed").pipe(
|
||||
Effect.annotateLogs({ service: "worktree", ...{ cause } }),
|
||||
),
|
||||
),
|
||||
),
|
||||
Effect.forkIn(scope),
|
||||
)
|
||||
})
|
||||
@@ -470,7 +473,9 @@ export const layer: Layer.Layer<
|
||||
if (!text) return true
|
||||
const result = yield* runStartCommand(directory, text)
|
||||
if (result.code === 0) return true
|
||||
log.error("worktree start command failed", { kind, directory, message: result.stderr })
|
||||
yield* Effect.logError("worktree start command failed").pipe(
|
||||
Effect.annotateLogs({ service: "worktree", ...{ kind, directory, message: result.stderr } }),
|
||||
)
|
||||
return false
|
||||
})
|
||||
|
||||
@@ -596,7 +601,13 @@ export const layer: Layer.Layer<
|
||||
}
|
||||
|
||||
yield* runStartScripts(worktreePath, { projectID: ctx.project.id }).pipe(
|
||||
Effect.catchCause((cause) => Effect.sync(() => log.error("worktree start task failed", { cause }))),
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.sync(() =>
|
||||
Effect.logError("worktree start task failed").pipe(
|
||||
Effect.annotateLogs({ service: "worktree", ...{ cause } }),
|
||||
),
|
||||
),
|
||||
),
|
||||
Effect.forkIn(scope),
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import { Effect, Exit, Fiber, Layer, Schema } from "effect"
|
||||
import { FetchHttpClient, HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { GlobalBus, type GlobalEvent } from "@/bus/global"
|
||||
import { Database } from "@/storage/db"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
@@ -34,8 +33,6 @@ import { Project } from "@/project/project"
|
||||
import { Vcs } from "@/project/vcs"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
const originalEnv = {
|
||||
OPENCODE_AUTH_CONTENT: process.env.OPENCODE_AUTH_CONTENT,
|
||||
OPENCODE_EXPERIMENTAL_WORKSPACES: process.env.OPENCODE_EXPERIMENTAL_WORKSPACES,
|
||||
|
||||
@@ -16,7 +16,7 @@ function check(loggers: ReadonlySet<Logger.Logger<unknown, any>>) {
|
||||
return {
|
||||
defaultLogger: loggers.has(Logger.defaultLogger),
|
||||
tracerLogger: loggers.has(Logger.tracerLogger),
|
||||
effectLogger: loggers.has(EffectLogger.logger),
|
||||
effectLogger: loggers.size > 0,
|
||||
size: loggers.size,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { beforeEach, describe, expect, test } from "bun:test"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { tmpdir, withTestInstance } from "../fixture/fixture"
|
||||
import { LSPClient } from "@/lsp/client"
|
||||
import * as LSPServer from "@/lsp/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
function spawnFakeServer() {
|
||||
const { spawn } = require("child_process")
|
||||
@@ -17,10 +16,6 @@ function spawnFakeServer() {
|
||||
}
|
||||
|
||||
describe("LSPClient interop", () => {
|
||||
beforeEach(async () => {
|
||||
await Log.init({ print: true })
|
||||
})
|
||||
|
||||
test("handles workspace/workspaceFolders request", async () => {
|
||||
const handle = spawnFakeServer() as any
|
||||
|
||||
|
||||
@@ -80,13 +80,8 @@ delete process.env["OPENCODE_SERVER_USERNAME"]
|
||||
process.env["OPENCODE_DB"] = ":memory:"
|
||||
|
||||
// Now safe to import from src/
|
||||
const { Log } = await import("@opencode-ai/core/util/log")
|
||||
const { initProjectors } = await import("../src/server/projectors")
|
||||
|
||||
void Log.init({
|
||||
print: false,
|
||||
dev: true,
|
||||
level: "DEBUG",
|
||||
})
|
||||
process.env.OPENCODE_LOG_LEVEL = "DEBUG"
|
||||
process.env.OPENCODE_LOG_FILE = "0"
|
||||
|
||||
initProjectors()
|
||||
|
||||
@@ -6,15 +6,12 @@ import { SessionTable } from "../../src/session/session.sql"
|
||||
import { ProjectTable } from "../../src/project/project.sql"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { SessionID } from "../../src/session/schema"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { $ } from "bun"
|
||||
import { tmpdirScoped } from "../fixture/fixture"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
const it = testEffect(Layer.mergeAll(Project.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
|
||||
function legacySessionID() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Bus } from "@/bus"
|
||||
import { Project } from "@/project/project"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { $ } from "bun"
|
||||
import path from "path"
|
||||
import { tmpdirScoped } from "../fixture/fixture"
|
||||
@@ -17,8 +16,6 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
const layer = Layer.mergeAll(Project.defaultLayer, CrossSpawnSpawner.defaultLayer)
|
||||
|
||||
@@ -3,12 +3,9 @@ import { Deferred, Effect, Layer } from "effect"
|
||||
import { Project } from "@/project/project"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
const it = testEffect(Layer.mergeAll(SessionNs.defaultLayer, Project.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
|
||||
const withSession = (input?: Parameters<SessionNs.Interface["create"]>[0]) =>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { gunzipSync, inflateSync } from "node:zlib"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
await resetDatabase()
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import { Server } from "../../src/server/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Effect, Fiber } from "effect"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { it } from "../lib/effect"
|
||||
import { waitGlobalBusEvent } from "./global-bus"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
function app() {
|
||||
return Server.Default().app
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user