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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/opencode/src/acp/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,12 @@ export class ACPSessionManager {
this.sessions.set(sessionId, session)
return session
}

remove(sessionId: string) {
const deleted = this.sessions.delete(sessionId)
if (deleted) {
log.info("removed session", { sessionId })
}
return deleted
}
}
6 changes: 6 additions & 0 deletions packages/opencode/src/file/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,10 @@ export namespace FileTime {
)
}
}

export function clearSession(sessionID: string) {
const current = state()
delete current.read[sessionID]
log.info("cleared session", { sessionID })
}
}
69 changes: 39 additions & 30 deletions packages/opencode/src/format/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,36 +102,45 @@ export namespace Format {

export function init() {
log.info("init")
Bus.subscribe(File.Event.Edited, async (payload) => {
const file = payload.properties.file
log.info("formatting", { file })
const ext = path.extname(file)

for (const item of await getFormatter(ext)) {
log.info("running", { command: item.command })
try {
const proc = Bun.spawn({
cmd: item.command.map((x) => x.replace("$FILE", file)),
cwd: Instance.directory,
env: { ...process.env, ...item.environment },
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
if (exit !== 0)
log.error("failed", {
command: item.command,
...item.environment,
})
} catch (error) {
log.error("failed to format file", {
error,
command: item.command,
...item.environment,
file,
})
}
}
})
Instance.state(
() => {
const unsubscribe = Bus.subscribe(File.Event.Edited, async (payload) => {
const file = payload.properties.file
log.info("formatting", { file })
const ext = path.extname(file)

for (const item of await getFormatter(ext)) {
log.info("running", { command: item.command })
try {
const proc = Bun.spawn({
cmd: item.command.map((x) => x.replace("$FILE", file)),
cwd: Instance.directory,
env: { ...process.env, ...item.environment },
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
if (exit !== 0)
log.error("failed", {
command: item.command,
...item.environment,
})
} catch (error) {
log.error("failed to format file", {
error,
command: item.command,
...item.environment,
file,
})
}
}
})
return { unsubscribe }
},
async (state) => {
state.unsubscribe()
},
)()
}
}
37 changes: 35 additions & 2 deletions packages/opencode/src/permission/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export namespace PermissionNext {
info: Request
resolve: () => void
reject: (e: any) => void
timeout: ReturnType<typeof setTimeout>
}
> = {}

Expand Down Expand Up @@ -143,10 +144,28 @@ export namespace PermissionNext {
id,
...request,
}

const timeout = setTimeout(
() => {
if (s.pending[id]) {
delete s.pending[id]
reject(new Error("Permission request timed out"))
}
},
5 * 60 * 1000,
)

s.pending[id] = {
info,
resolve,
reject,
resolve: () => {
clearTimeout(timeout)
resolve()
},
reject: (e) => {
clearTimeout(timeout)
reject(e)
},
timeout,
}
Bus.publish(Event.Asked, info)
})
Expand All @@ -166,6 +185,7 @@ export namespace PermissionNext {
const s = await state()
const existing = s.pending[input.requestID]
if (!existing) return
clearTimeout(existing.timeout)
delete s.pending[input.requestID]
Bus.publish(Event.Replied, {
sessionID: existing.info.sessionID,
Expand All @@ -178,6 +198,7 @@ export namespace PermissionNext {
const sessionID = existing.info.sessionID
for (const [id, pending] of Object.entries(s.pending)) {
if (pending.info.sessionID === sessionID) {
clearTimeout(pending.timeout)
delete s.pending[id]
Bus.publish(Event.Replied, {
sessionID: pending.info.sessionID,
Expand Down Expand Up @@ -211,6 +232,7 @@ export namespace PermissionNext {
(pattern) => evaluate(pending.info.permission, pattern, s.approved).action === "allow",
)
if (!ok) continue
clearTimeout(pending.timeout)
delete s.pending[id]
Bus.publish(Event.Replied, {
sessionID: pending.info.sessionID,
Expand Down Expand Up @@ -277,4 +299,15 @@ export namespace PermissionNext {
export async function list() {
return state().then((x) => Object.values(x.pending).map((x) => x.info))
}

export async function clearSession(sessionID: string) {
const s = await state()
for (const [id, pending] of Object.entries(s.pending)) {
if (pending.info.sessionID === sessionID) {
clearTimeout(pending.timeout)
delete s.pending[id]
pending.reject(new Error("Session ended"))
}
}
}
}
23 changes: 16 additions & 7 deletions packages/opencode/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,22 @@ export namespace Plugin {
// @ts-expect-error this is because we haven't moved plugin to sdk v2
await hook.config?.(config)
}
Bus.subscribeAll(async (input) => {
const hooks = await state().then((x) => x.hooks)
for (const hook of hooks) {
hook["event"]?.({
event: input,

Instance.state(
() => {
const unsubscribe = Bus.subscribeAll(async (input) => {
const hooks = await state().then((x) => x.hooks)
for (const hook of hooks) {
hook["event"]?.({
event: input,
})
}
})
}
})
return { unsubscribe }
},
async (state) => {
state.unsubscribe()
},
)()
}
}
5 changes: 3 additions & 2 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Env } from "../env"
import { Instance } from "../project/instance"
import { Flag } from "../flag/flag"
import { iife } from "@/util/iife"
import { LRUMap } from "@/util/lru"

// Direct imports for bundled providers
import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock"
Expand Down Expand Up @@ -689,11 +690,11 @@ export namespace Provider {
}

const providers: { [providerID: string]: Info } = {}
const languages = new Map<string, LanguageModelV2>()
const languages = new LRUMap<string, LanguageModelV2>(100)
const modelLoaders: {
[providerID: string]: CustomModelLoader
} = {}
const sdk = new Map<number, SDK>()
const sdk = new LRUMap<number, SDK>(50)

log.info("init")

Expand Down
36 changes: 34 additions & 2 deletions packages/opencode/src/question/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export namespace Question {
info: Request
resolve: (answers: Answer[]) => void
reject: (e: any) => void
timeout: ReturnType<typeof setTimeout>
}
> = {}

Expand All @@ -111,10 +112,28 @@ export namespace Question {
questions: input.questions,
tool: input.tool,
}

const timeout = setTimeout(
() => {
if (s.pending[id]) {
delete s.pending[id]
reject(new Error("Question timed out"))
}
},
5 * 60 * 1000,
)

s.pending[id] = {
info,
resolve,
reject,
resolve: (answers) => {
clearTimeout(timeout)
resolve(answers)
},
reject: (e) => {
clearTimeout(timeout)
reject(e)
},
timeout,
}
Bus.publish(Event.Asked, info)
})
Expand All @@ -127,6 +146,7 @@ export namespace Question {
log.warn("reply for unknown request", { requestID: input.requestID })
return
}
clearTimeout(existing.timeout)
delete s.pending[input.requestID]

log.info("replied", { requestID: input.requestID, answers: input.answers })
Expand All @@ -147,6 +167,7 @@ export namespace Question {
log.warn("reject for unknown request", { requestID })
return
}
clearTimeout(existing.timeout)
delete s.pending[requestID]

log.info("rejected", { requestID })
Expand All @@ -168,4 +189,15 @@ export namespace Question {
export async function list() {
return state().then((x) => Object.values(x.pending).map((x) => x.info))
}

export async function clearSession(sessionID: string) {
const s = await state()
for (const [id, pending] of Object.entries(s.pending)) {
if (pending.info.sessionID === sessionID) {
clearTimeout(pending.timeout)
delete s.pending[id]
pending.reject(new Error("Session ended"))
}
}
}
}
5 changes: 4 additions & 1 deletion packages/opencode/src/session/compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,13 @@ export namespace SessionCompaction {
if (pruned > PRUNE_MINIMUM) {
for (const part of toPrune) {
if (part.state.status === "completed") {
part.state.output = "[Old tool result content cleared]"
part.state.attachments = []
part.state.time.compacted = Date.now()
await Session.updatePart(part)
}
}
log.info("pruned", { count: toPrune.length })
log.info("pruned", { count: toPrune.length, estimatedMB: Math.round(pruned / 1024 / 1024) })
}
}

Expand Down Expand Up @@ -189,6 +191,7 @@ export namespace SessionCompaction {
}
if (processor.message.error) return "stop"
Bus.publish(Event.Compacted, { sessionID: input.sessionID })
if (global.gc) global.gc()
return "continue"
}

Expand Down
7 changes: 7 additions & 0 deletions packages/opencode/src/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import { Snapshot } from "@/snapshot"

import type { Provider } from "@/provider/provider"
import { PermissionNext } from "@/permission/next"
import { Question } from "@/question"
import { Global } from "@/global"
import { FileTime } from "@/file/time"

export namespace Session {
const log = Log.create({ service: "session" })
Expand Down Expand Up @@ -341,6 +343,11 @@ export namespace Session {
await remove(child.id)
}
await unshare(sessionID).catch(() => {})

FileTime.clearSession(sessionID)
await PermissionNext.clearSession(sessionID)
await Question.clearSession(sessionID)

for (const msg of await Storage.list(["message", sessionID])) {
for (const part of await Storage.list(["part", msg.at(-1)!])) {
await Storage.remove(part)
Expand Down
Loading