Skip to content

Commit 54d83b5

Browse files
authored
skill: use Effect.cached for load deduplication (anomalyco#19165)
1 parent fac25b9 commit 54d83b5

3 files changed

Lines changed: 49 additions & 73 deletions

File tree

packages/opencode/src/mcp/auth.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import z from "zod"
33
import { Global } from "../global"
44
import { Effect, Layer, ServiceMap } from "effect"
55
import { AppFileSystem } from "@/filesystem"
6-
import { makeRunPromise } from "@/effect/run-service"
6+
import { makeRuntime } from "@/effect/run-service"
77

88
export namespace McpAuth {
99
export const Tokens = z.object({
@@ -143,7 +143,7 @@ export namespace McpAuth {
143143

144144
const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
145145

146-
const runPromise = makeRunPromise(Service, defaultLayer)
146+
const { runPromise } = makeRuntime(Service, defaultLayer)
147147

148148
// Async facades for backward compat (used by McpOAuthProvider, CLI)
149149

packages/opencode/src/mcp/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { TuiEvent } from "@/cli/cmd/tui/event"
2626
import open from "open"
2727
import { Effect, Layer, Option, ServiceMap, Stream } from "effect"
2828
import { InstanceState } from "@/effect/instance-state"
29-
import { makeRunPromise } from "@/effect/run-service"
29+
import { makeRuntime } from "@/effect/run-service"
3030
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
3131
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
3232
import { NodeFileSystem } from "@effect/platform-node"
@@ -893,7 +893,7 @@ export namespace MCP {
893893
Layer.provide(NodePath.layer),
894894
)
895895

896-
const runPromise = makeRunPromise(Service, defaultLayer)
896+
const { runPromise } = makeRuntime(Service, defaultLayer)
897897

898898
// --- Async facade functions ---
899899

packages/opencode/src/skill/index.ts

Lines changed: 45 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,6 @@ export namespace Skill {
5454
type State = {
5555
skills: Record<string, Info>
5656
dirs: Set<string>
57-
task?: Promise<void>
58-
}
59-
60-
type Cache = State & {
61-
ensure: () => Promise<void>
6257
}
6358

6459
export interface Interface {
@@ -116,66 +111,47 @@ export namespace Skill {
116111
})
117112
}
118113

119-
// TODO: Migrate to Effect
120-
const create = (discovery: Discovery.Interface, directory: string, worktree: string): Cache => {
121-
const state: State = {
122-
skills: {},
123-
dirs: new Set<string>(),
124-
}
125-
126-
const load = async () => {
127-
if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) {
128-
for (const dir of EXTERNAL_DIRS) {
129-
const root = path.join(Global.Path.home, dir)
130-
if (!(await Filesystem.isDir(root))) continue
131-
await scan(state, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global" })
132-
}
133-
134-
for await (const root of Filesystem.up({
135-
targets: EXTERNAL_DIRS,
136-
start: directory,
137-
stop: worktree,
138-
})) {
139-
await scan(state, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "project" })
140-
}
114+
async function loadSkills(state: State, discovery: Discovery.Interface, directory: string, worktree: string) {
115+
if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) {
116+
for (const dir of EXTERNAL_DIRS) {
117+
const root = path.join(Global.Path.home, dir)
118+
if (!(await Filesystem.isDir(root))) continue
119+
await scan(state, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global" })
141120
}
142121

143-
for (const dir of await Config.directories()) {
144-
await scan(state, dir, OPENCODE_SKILL_PATTERN)
122+
for await (const root of Filesystem.up({
123+
targets: EXTERNAL_DIRS,
124+
start: directory,
125+
stop: worktree,
126+
})) {
127+
await scan(state, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "project" })
145128
}
129+
}
146130

147-
const cfg = await Config.get()
148-
for (const item of cfg.skills?.paths ?? []) {
149-
const expanded = item.startsWith("~/") ? path.join(os.homedir(), item.slice(2)) : item
150-
const dir = path.isAbsolute(expanded) ? expanded : path.join(directory, expanded)
151-
if (!(await Filesystem.isDir(dir))) {
152-
log.warn("skill path not found", { path: dir })
153-
continue
154-
}
155-
156-
await scan(state, dir, SKILL_PATTERN)
157-
}
131+
for (const dir of await Config.directories()) {
132+
await scan(state, dir, OPENCODE_SKILL_PATTERN)
133+
}
158134

159-
for (const url of cfg.skills?.urls ?? []) {
160-
for (const dir of await Effect.runPromise(discovery.pull(url))) {
161-
state.dirs.add(dir)
162-
await scan(state, dir, SKILL_PATTERN)
163-
}
135+
const cfg = await Config.get()
136+
for (const item of cfg.skills?.paths ?? []) {
137+
const expanded = item.startsWith("~/") ? path.join(os.homedir(), item.slice(2)) : item
138+
const dir = path.isAbsolute(expanded) ? expanded : path.join(directory, expanded)
139+
if (!(await Filesystem.isDir(dir))) {
140+
log.warn("skill path not found", { path: dir })
141+
continue
164142
}
165143

166-
log.info("init", { count: Object.keys(state.skills).length })
144+
await scan(state, dir, SKILL_PATTERN)
167145
}
168146

169-
const ensure = () => {
170-
if (state.task) return state.task
171-
state.task = load().catch((err) => {
172-
state.task = undefined
173-
throw err
174-
})
175-
return state.task
147+
for (const url of cfg.skills?.urls ?? []) {
148+
for (const dir of await Effect.runPromise(discovery.pull(url))) {
149+
state.dirs.add(dir)
150+
await scan(state, dir, SKILL_PATTERN)
151+
}
176152
}
177153

178-
return { ...state, ensure }
154+
log.info("init", { count: Object.keys(state.skills).length })
179155
}
180156

181157
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Skill") {}
@@ -185,33 +161,33 @@ export namespace Skill {
185161
Effect.gen(function* () {
186162
const discovery = yield* Discovery.Service
187163
const state = yield* InstanceState.make(
188-
Effect.fn("Skill.state")((ctx) => Effect.sync(() => create(discovery, ctx.directory, ctx.worktree))),
164+
Effect.fn("Skill.state")((ctx) =>
165+
Effect.gen(function* () {
166+
const s: State = { skills: {}, dirs: new Set() }
167+
yield* Effect.promise(() => loadSkills(s, discovery, ctx.directory, ctx.worktree))
168+
return s
169+
}),
170+
),
189171
)
190172

191-
const ensure = Effect.fn("Skill.ensure")(function* () {
192-
const cache = yield* InstanceState.get(state)
193-
yield* Effect.promise(() => cache.ensure())
194-
return cache
195-
})
196-
197173
const get = Effect.fn("Skill.get")(function* (name: string) {
198-
const cache = yield* ensure()
199-
return cache.skills[name]
174+
const s = yield* InstanceState.get(state)
175+
return s.skills[name]
200176
})
201177

202178
const all = Effect.fn("Skill.all")(function* () {
203-
const cache = yield* ensure()
204-
return Object.values(cache.skills)
179+
const s = yield* InstanceState.get(state)
180+
return Object.values(s.skills)
205181
})
206182

207183
const dirs = Effect.fn("Skill.dirs")(function* () {
208-
const cache = yield* ensure()
209-
return Array.from(cache.dirs)
184+
const s = yield* InstanceState.get(state)
185+
return Array.from(s.dirs)
210186
})
211187

212188
const available = Effect.fn("Skill.available")(function* (agent?: Agent.Info) {
213-
const cache = yield* ensure()
214-
const list = Object.values(cache.skills).toSorted((a, b) => a.name.localeCompare(b.name))
189+
const s = yield* InstanceState.get(state)
190+
const list = Object.values(s.skills).toSorted((a, b) => a.name.localeCompare(b.name))
215191
if (!agent) return list
216192
return list.filter((skill) => Permission.evaluate("skill", skill.name, agent.permission).action !== "deny")
217193
})

0 commit comments

Comments
 (0)