Skip to content

Commit ce529a2

Browse files
fix(opencode): reset caches and emit hot reload event
1 parent 5f4b9d3 commit ce529a2

7 files changed

Lines changed: 119 additions & 14 deletions

File tree

packages/opencode/src/agent/agent.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,10 @@ export namespace Agent {
253253
return state().then((x) => x[agent])
254254
}
255255

256+
export async function reset() {
257+
await state.reset()
258+
}
259+
256260
export async function list() {
257261
const cfg = await Config.get()
258262
return pipe(

packages/opencode/src/command/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ export namespace Command {
144144
return state().then((x) => x[name])
145145
}
146146

147+
export async function reset() {
148+
await state.reset()
149+
}
150+
147151
export async function list() {
148152
return state().then((x) => Object.values(x))
149153
}

packages/opencode/src/config/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1361,6 +1361,10 @@ export namespace Config {
13611361
return state().then((x) => x.config)
13621362
}
13631363

1364+
export async function reset() {
1365+
await state.reset()
1366+
}
1367+
13641368
export async function getGlobal() {
13651369
return global()
13661370
}

packages/opencode/src/project/hotreload.ts

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
11
import path from "path"
22
import { Bus } from "@/bus"
3+
import { BusEvent } from "@/bus/bus-event"
4+
import { Agent } from "@/agent/agent"
5+
import { Command } from "@/command"
6+
import { Config } from "@/config/config"
37
import { FileWatcher } from "@/file/watcher"
48
import { Flag } from "@/flag/flag"
9+
import { SessionStatus } from "@/session/status"
10+
import { Skill } from "@/skill"
511
import { Log } from "@/util/log"
612
import { Instance } from "./instance"
13+
import z from "zod"
714

815
export namespace HotReload {
916
const log = Log.create({ service: "project.hotreload" })
1017

18+
export const Event = {
19+
Applied: BusEvent.define(
20+
"opencode.hotreload.applied",
21+
z.object({
22+
file: z.string(),
23+
event: z.enum(["add", "change", "unlink"]),
24+
}),
25+
),
26+
}
27+
1128
const watched = new Set([
1229
"agent",
1330
"agents",
@@ -99,45 +116,80 @@ export namespace HotReload {
99116
let timer: ReturnType<typeof setTimeout> | undefined
100117
let busy = false
101118
let last = 0
119+
let queued = false
102120
let latest:
103121
| {
104122
file: string
105123
event: "add" | "change" | "unlink"
106124
}
107125
| undefined
108126

109-
const flush = () => {
127+
const active = () =>
128+
Object.values(SessionStatus.list()).filter((status) => status.type === "busy" || status.type === "retry").length
129+
130+
const reload = async () => {
131+
await Config.reset()
132+
await Skill.reset()
133+
await Agent.reset()
134+
await Command.reset()
135+
}
136+
137+
const flush = (reason: "timer" | "session") => {
110138
timer = undefined
111139
if (busy) return
112140

141+
const hit = latest
142+
if (!hit) return
143+
144+
const sessions = active()
145+
if (sessions > 0) {
146+
if (!queued) {
147+
log.info("hot reload queued", {
148+
file: hit.file,
149+
event: hit.event,
150+
sessions,
151+
})
152+
}
153+
queued = true
154+
return
155+
}
156+
113157
const now = Date.now()
114158
const wait = cooldown - (now - last)
115159
if (wait > 0) {
116-
timer = setTimeout(flush, wait)
160+
timer = setTimeout(() => flush(reason), wait)
117161
return
118162
}
119163

120-
const hit = latest
121-
if (!hit) return
122-
123164
busy = true
165+
queued = false
166+
latest = undefined
124167
last = now
125-
log.info("hot reload triggered", { file: hit.file, event: hit.event })
126-
void Instance.dispose()
168+
log.info("hot reload triggered", { file: hit.file, event: hit.event, reason })
169+
void reload()
170+
.then(() =>
171+
Bus.publish(Event.Applied, {
172+
file: hit.file,
173+
event: hit.event,
174+
}),
175+
)
127176
.catch((error) => {
128177
log.error("hot reload failed", { error, file: hit.file, event: hit.event })
129178
})
130179
.finally(() => {
131180
busy = false
181+
if (!latest) return
182+
if (timer) clearTimeout(timer)
183+
timer = setTimeout(() => flush("timer"), debounce)
132184
})
133185
}
134186

135187
const schedule = () => {
136188
if (timer) clearTimeout(timer)
137-
timer = setTimeout(flush, debounce)
189+
timer = setTimeout(() => flush("timer"), debounce)
138190
}
139191

140-
const unsub = Bus.subscribe(FileWatcher.Event.Updated, (event) => {
192+
const unsubFile = Bus.subscribe(FileWatcher.Event.Updated, (event) => {
141193
const rel = classify(Instance.directory, event.properties.file)
142194
if (!rel) return
143195
latest = {
@@ -147,9 +199,16 @@ export namespace HotReload {
147199
schedule()
148200
})
149201

202+
const unsubSession = Bus.subscribe(SessionStatus.Event.Status, () => {
203+
if (!queued) return
204+
if (timer) return
205+
timer = setTimeout(() => flush("session"), 0)
206+
})
207+
150208
log.info("hot reload enabled", { debounce, cooldown })
151209
return {
152-
unsub,
210+
unsubFile,
211+
unsubSession,
153212
clear() {
154213
if (!timer) return
155214
clearTimeout(timer)
@@ -158,7 +217,8 @@ export namespace HotReload {
158217
}
159218
},
160219
async (entry) => {
161-
entry.unsub?.()
220+
entry.unsubFile?.()
221+
entry.unsubSession?.()
162222
entry.clear?.()
163223
},
164224
)

packages/opencode/src/project/instance.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const Instance = {
6363
if (Instance.worktree === "/") return false
6464
return Filesystem.contains(Instance.worktree, filepath)
6565
},
66-
state<S>(init: () => S, dispose?: (state: Awaited<S>) => Promise<void>): () => S {
66+
state<S>(init: () => S, dispose?: (state: Awaited<S>) => Promise<void>): State.Accessor<S> {
6767
return State.create(() => Instance.directory, init, dispose)
6868
},
6969
async dispose() {

packages/opencode/src/project/state.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { Log } from "@/util/log"
22

33
export namespace State {
4+
export type Accessor<S> = (() => S) & {
5+
reset: () => Promise<void>
6+
}
7+
48
interface Entry {
59
state: any
610
dispose?: (state: any) => Promise<void>
@@ -9,8 +13,8 @@ export namespace State {
913
const log = Log.create({ service: "state" })
1014
const recordsByKey = new Map<string, Map<any, Entry>>()
1115

12-
export function create<S>(root: () => string, init: () => S, dispose?: (state: Awaited<S>) => Promise<void>) {
13-
return () => {
16+
export function create<S>(root: () => string, init: () => S, dispose?: (state: Awaited<S>) => Promise<void>): Accessor<S> {
17+
const fn = (() => {
1418
const key = root()
1519
let entries = recordsByKey.get(key)
1620
if (!entries) {
@@ -25,7 +29,32 @@ export namespace State {
2529
dispose,
2630
})
2731
return state
32+
}) as Accessor<S>
33+
34+
fn.reset = async () => {
35+
await disposeInit(root(), init)
2836
}
37+
38+
return fn
39+
}
40+
41+
async function disposeInit(key: string, init: any) {
42+
const entries = recordsByKey.get(key)
43+
if (!entries) return
44+
const entry = entries.get(init)
45+
if (!entry) return
46+
47+
if (entry.dispose) {
48+
await Promise.resolve(entry.state)
49+
.then((state) => entry.dispose!(state))
50+
.catch((error) => {
51+
const label = typeof init === "function" ? init.name : String(init)
52+
log.error("Error while disposing state:", { error, key, init: label })
53+
})
54+
}
55+
56+
entries.delete(init)
57+
if (!entries.size) recordsByKey.delete(key)
2958
}
3059

3160
export async function dispose(key: string) {

packages/opencode/src/skill/skill.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ export namespace Skill {
178178
return state().then((x) => x.skills[name])
179179
}
180180

181+
export async function reset() {
182+
await state.reset()
183+
}
184+
181185
export async function all() {
182186
return state().then((x) => Object.values(x.skills))
183187
}

0 commit comments

Comments
 (0)