Skip to content

Commit 89cdc3b

Browse files
committed
fix(opencode): harden memory bootstrap fallback
1 parent 1430b77 commit 89cdc3b

2 files changed

Lines changed: 144 additions & 88 deletions

File tree

packages/opencode/src/memory/memory.ts

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { KeyedMutex } from "@opencode-ai/core/effect/keyed-mutex"
55
import { SessionV1 } from "@opencode-ai/core/v1/session"
66
import { Context, Duration, Effect, Layer, Option, Ref, Schema, Semaphore } from "effect"
77
import { stringify } from "yaml"
8-
import { Agent } from "@/agent/agent"
98
import { Config } from "@/config/config"
109
import { Provider } from "@/provider/provider"
1110
import { Project } from "@/project/project"
@@ -63,17 +62,10 @@ export class ControllerError extends Schema.TaggedErrorClass<ControllerError>()(
6362
export const layer: Layer.Layer<
6463
Service,
6564
never,
66-
| Agent.Service
67-
| Config.Service
68-
| Provider.Service
69-
| Project.Service
70-
| MemoryConfig.Service
71-
| MemoryModel.Service
72-
| MemoryStore.Service
65+
Config.Service | Provider.Service | Project.Service | MemoryConfig.Service | MemoryModel.Service | MemoryStore.Service
7366
> = Layer.effect(
7467
Service,
7568
Effect.gen(function* () {
76-
const agent = yield* Agent.Service
7769
const config = yield* Config.Service
7870
const provider = yield* Provider.Service
7971
const project = yield* Project.Service
@@ -85,37 +77,24 @@ export const layer: Layer.Layer<
8577
const locks = KeyedMutex.makeUnsafe<string>()
8678
const state = yield* InstanceState.make(() => Effect.succeed({ sessions: new Map<SessionID, SessionCache>() }))
8779

88-
const models = Effect.fn("Memory.models")(function* () {
80+
const availableModels = Effect.fn("Memory.availableModels")(function* () {
8981
const providers = yield* provider.list()
90-
return Object.values(providers)
91-
.flatMap((info) =>
82+
return new Set(
83+
Object.values(providers).flatMap((info) =>
9284
Object.values(info.models)
9385
.filter((model) => model.capabilities.input.text && model.capabilities.output.text)
94-
.map((model) => ({
95-
id: `${model.providerID}/${model.id}`,
96-
name: model.name,
97-
input_cost: model.cost.input,
98-
output_cost: model.cost.output,
99-
context_limit: model.limit.context,
100-
output_limit: model.limit.output,
101-
})),
102-
)
103-
.sort((a, b) => a.input_cost + a.output_cost - (b.input_cost + b.output_cost) || a.id.localeCompare(b.id))
86+
.map((model) => `${model.providerID}/${model.id}`),
87+
),
88+
)
10489
})
10590

10691
const selectBootstrapModel = Effect.fn("Memory.selectBootstrapModel")(function* (
107-
candidates: Effect.Success<ReturnType<typeof models>>,
92+
available: Effect.Success<ReturnType<typeof availableModels>>,
10893
conversationModel?: string,
10994
) {
110-
if (candidates.length === 0)
111-
return yield* new ControllerError({ message: "No configured text models for MEMORY" })
112-
const available = new Set(candidates.map((candidate) => candidate.id))
113-
const smallModel = (yield* config.get()).small_model
114-
if (smallModel && available.has(smallModel)) return smallModel
115-
const compaction = yield* agent.get("compaction")
116-
const compactionModel = compaction.model
117-
? `${compaction.model.providerID}/${compaction.model.modelID}`
118-
: undefined
95+
const settings = yield* config.get()
96+
if (settings.small_model && available.has(settings.small_model)) return settings.small_model
97+
const compactionModel = settings.agent?.compaction?.model
11998
if (compactionModel && available.has(compactionModel)) return compactionModel
12099
const defaultModel = yield* provider.defaultModel().pipe(Effect.option)
121100
const fallback = Option.isSome(defaultModel)
@@ -127,11 +106,11 @@ export const layer: Layer.Layer<
127106
})
128107

129108
const selectConfiguration = Effect.fn("Memory.selectConfiguration")(function* (
130-
candidates: Effect.Success<ReturnType<typeof models>>,
109+
available: Effect.Success<ReturnType<typeof availableModels>>,
131110
current?: MemorySchema.Config,
132111
conversationModel?: string,
133112
) {
134-
const selected = yield* selectBootstrapModel(candidates, conversationModel)
113+
const selected = yield* selectBootstrapModel(available, conversationModel)
135114
if (current) return MemorySchema.updateConfig(current, { model: selected })
136115
return {
137116
schema_version: MemorySchema.SCHEMA_VERSION,
@@ -151,19 +130,19 @@ export const layer: Layer.Layer<
151130
config: MemorySchema.Config,
152131
conversationModel?: string,
153132
) {
154-
const candidates = yield* models()
155-
if (candidates.some((candidate) => candidate.id === config.model)) return config
133+
const available = yield* availableModels()
134+
if (available.has(config.model)) return config
156135
yield* Effect.logWarning("configured MEMORY model is unavailable — selecting a replacement", {
157136
model: config.model,
158137
})
159-
return yield* selectConfiguration(candidates, config, conversationModel)
138+
return yield* selectConfiguration(available, config, conversationModel)
160139
})
161140

162141
const initializeGlobal = Effect.fn("Memory.initializeGlobal")(function* (conversationModel?: string) {
163142
const existing = yield* configStore.loadGlobal()
164143
const config = existing
165144
? yield* ensureConfiguredModel(existing.config, conversationModel)
166-
: yield* selectConfiguration(yield* models(), undefined, conversationModel)
145+
: yield* selectConfiguration(yield* availableModels(), undefined, conversationModel)
167146
if (existing?.config.model === config.model) return
168147
const created = yield* configStore.writeGlobal(config, existing?.path)
169148
if (created) yield* Effect.logInfo("global MEMORY config initialized", { model: config.model })
@@ -325,12 +304,16 @@ export const layer: Layer.Layer<
325304
}) {
326305
const user = latestRealUser(input.messages)
327306
if (!user) return
307+
const currentUser = currentRealUser(input.messages)
328308
const configured = yield* configuration()
329309
if (!configured) {
330310
yield* clearSession(input.sessionID)
331311
return
332312
}
333-
if (!configured.loaded) yield* initUnsafe(`${user.info.model.providerID}/${user.info.model.modelID}`)
313+
if (currentUser)
314+
yield* initUnsafe(`${currentUser.info.model.providerID}/${currentUser.info.model.modelID}`).pipe(
315+
Effect.catchCause((cause) => Effect.logWarning("global MEMORY init failed", { cause })),
316+
)
334317
const current = yield* active()
335318
if (!current) {
336319
yield* clearSession(input.sessionID)
@@ -541,7 +524,7 @@ export const layer: Layer.Layer<
541524
const value = initial.loaded
542525
? initial
543526
: yield* Effect.gen(function* () {
544-
yield* initializeGlobal()
527+
yield* initUnsafe()
545528
return (yield* configuration()) ?? initial
546529
})
547530
if (!value.loaded) return "Memory remains off" as const
@@ -581,7 +564,6 @@ export const layer: Layer.Layer<
581564

582565
export const defaultLayer: Layer.Layer<Service> = Layer.suspend(() =>
583566
layer.pipe(
584-
Layer.provide(Agent.defaultLayer),
585567
Layer.provide(Config.defaultLayer),
586568
Layer.provide(Provider.defaultLayer),
587569
Layer.provide(Project.defaultLayer),
@@ -592,7 +574,6 @@ export const defaultLayer: Layer.Layer<Service> = Layer.suspend(() =>
592574
)
593575

594576
export const node = LayerNode.make(layer, [
595-
Agent.node,
596577
Config.node,
597578
Provider.node,
598579
Project.node,
@@ -698,6 +679,16 @@ function maintenanceEvidence(messages: SessionV1.WithParts[]) {
698679
function latestRealUser(messages: SessionV1.WithParts[]) {
699680
const user = messages.findLast(isRealUser)
700681
if (!user) return undefined
682+
return userInput(user)
683+
}
684+
685+
function currentRealUser(messages: SessionV1.WithParts[]) {
686+
const user = messages.findLast((message) => message.info.role === "user")
687+
if (!user || !isRealUser(user)) return undefined
688+
return userInput(user)
689+
}
690+
691+
function userInput(user: SessionV1.WithParts & { info: SessionV1.User }) {
701692
return {
702693
info: user.info,
703694
text: cleanText(

0 commit comments

Comments
 (0)