Skip to content
Merged
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
7 changes: 4 additions & 3 deletions packages/opencode/src/memory/admission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { MemoryConfig } from "./config"
import { MemoryHome } from "./home"
import { MemoryIdentityFence } from "./identity-fence"
import { MemoryPaths } from "./paths"
import { MemorySchema } from "./schema"
import { MemoryStore } from "./store"

const Code = Schema.Literals([
Expand Down Expand Up @@ -73,7 +74,7 @@ type TopicCandidate = {

type ConfigCandidate = {
readonly file: string
readonly config: ReturnType<typeof MemoryConfig.normalizeConfig> | undefined
readonly config: MemorySchema.Config | undefined
}

export const layer = Layer.effect(
Expand Down Expand Up @@ -228,7 +229,7 @@ export const layer = Layer.effect(
const decoded = MemoryConfig.decodeConfig(text)
return {
file,
config: Option.isSome(decoded) ? MemoryConfig.normalizeConfig(decoded.value) : undefined,
config: Option.isSome(decoded) ? decoded.value : undefined,
} satisfies ConfigCandidate
}),
{ concurrency: 4 },
Expand All @@ -248,7 +249,7 @@ export const layer = Layer.effect(
if (text === undefined) return true
const decoded = MemoryConfig.decodeConfig(text)
if (Option.isNone(decoded)) return false
return same(MemoryConfig.normalizeConfig(decoded.value), candidate.config)
return same(decoded.value, candidate.config)
})

const removeValidated = Effect.fnUntraced(function* (candidates: ReadonlyArray<ConfigCandidate>) {
Expand Down
18 changes: 5 additions & 13 deletions packages/opencode/src/memory/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,7 @@ export const layer = Layer.effect(
yield* Effect.logWarning("memory config is invalid — ignoring", { path: found.path })
return undefined
}
if (decoded.value.topic_limit === decoded.value.topic_limit_floor) return decoded.value
const config = normalizeConfig(decoded.value)
yield* flock.withLock(MemoryFile.atomicWrite(fs, found.path, serialize(config)), writeLockKey(found.path))
return config
return decoded.value
})

const load = Effect.fn("MemoryConfig.load")(function* (projectDir: string) {
Expand Down Expand Up @@ -186,13 +183,8 @@ export function decodeConfig(text: string) {
const errors: ParseError[] = []
const value = parse(text, errors, { allowTrailingComma: true })
if (errors.length > 0) return Option.none<MemorySchema.Config>()
const decoded = Schema.decodeUnknownOption(MemorySchema.Config)(value ?? {})
if (Option.isNone(decoded) || decoded.value.topic_limit < decoded.value.topic_limit_floor)
return Option.none<MemorySchema.Config>()
return decoded
}

export function normalizeConfig(config: MemorySchema.Config) {
if (config.topic_limit === config.topic_limit_floor) return config
return MemorySchema.updateConfig(config, { topic_limit_floor: config.topic_limit })
// Legacy files may still carry the removed topic_limit_floor knob — Schema
// Struct decoding tolerates the extra property, so such files stay valid
// and the dead value is simply dropped on the next write.
return Schema.decodeUnknownOption(MemorySchema.Config)(value ?? {})
}
1 change: 0 additions & 1 deletion packages/opencode/src/memory/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ export const layer: Layer.Layer<
enabled: true,
model: selected,
topic_limit: 10,
topic_limit_floor: 10,
turn_interval: 5,
injection: {
max_topics: MemorySchema.MAX_INJECTION_TOPICS,
Expand Down
7 changes: 1 addition & 6 deletions packages/opencode/src/memory/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@ export class Config extends Schema.Class<Config>("MemoryConfig")({
Schema.isInt(),
Schema.isBetween({ minimum: MIN_TOPIC_LIMIT, maximum: MAX_TOPIC_LIMIT }),
),
topic_limit_floor: Schema.Number.check(
Schema.isInt(),
Schema.isBetween({ minimum: MIN_TOPIC_LIMIT, maximum: MAX_TOPIC_LIMIT }),
),
turn_interval: Schema.Number.check(
Schema.isInt(),
Schema.isBetween({ minimum: MIN_TURN_INTERVAL, maximum: MAX_TURN_INTERVAL }),
Expand All @@ -50,14 +46,13 @@ export class Config extends Schema.Class<Config>("MemoryConfig")({

export function updateConfig(
config: Config,
updates: { enabled?: boolean; model?: string; topic_limit_floor?: number },
updates: { enabled?: boolean; model?: string },
) {
return new Config({
schema_version: config.schema_version,
enabled: updates.enabled ?? config.enabled,
model: updates.model ?? config.model,
topic_limit: config.topic_limit,
topic_limit_floor: updates.topic_limit_floor ?? config.topic_limit_floor,
turn_interval: config.turn_interval,
injection: config.injection,
})
Expand Down
1 change: 0 additions & 1 deletion packages/opencode/test/lib/cli-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,6 @@ export function withCliFixture<A, E>(
enabled: true,
model: testModelID,
topic_limit: 10,
topic_limit_floor: 10,
turn_interval: 5,
injection: { max_topics: 3, max_tokens: 1_200 },
}),
Expand Down
54 changes: 54 additions & 0 deletions packages/opencode/test/memory/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, test } from "bun:test"
import { decodeConfig } from "@/memory/config"
import { MemorySchema, updateConfig } from "@/memory/schema"

// memory-config-fidelity: topic_limit_floor was a dead knob — no runtime
// consumer read it, and readConfig silently rewrote any floor ≠ limit file to
// floor := limit on every load, overwriting explicit user values. The field
// must be gone: legacy files carrying it still decode (extra property is
// tolerated), and decoded/updated configs never contain it.

const valid = {
schema_version: 1,
enabled: true,
model: "p/m",
topic_limit: 10,
turn_interval: 5,
injection: { max_topics: 3, max_tokens: 1200 },
}

describe("MemoryConfig decode/update — topic_limit_floor removed", () => {
test("legacy file with a divergent floor still decodes (field ignored)", () => {
const decoded = decodeConfig(JSON.stringify({ ...valid, topic_limit_floor: 30 }))
expect(decoded._tag).toBe("Some")
if (decoded._tag === "Some") {
expect(Object.keys(decoded.value)).not.toContain("topic_limit_floor")
}
})

test("decoded config never carries the field", () => {
const decoded = decodeConfig(JSON.stringify(valid))
expect(decoded._tag).toBe("Some")
if (decoded._tag === "Some") {
expect(Object.keys(decoded.value)).not.toContain("topic_limit_floor")
}
})

test("updateConfig output never carries the field", () => {
const decoded = decodeConfig(JSON.stringify(valid))
expect(decoded._tag).toBe("Some")
if (decoded._tag === "Some") {
const next = updateConfig(decoded.value, { enabled: false })
expect(Object.keys(next)).not.toContain("topic_limit_floor")
}
})

test("schema bounds for the live knobs are preserved", () => {
expect(decodeConfig(JSON.stringify({ ...valid, topic_limit: 9 }))._tag).toBe("None")
expect(decodeConfig(JSON.stringify({ ...valid, turn_interval: 0 }))._tag).toBe("None")
expect(decodeConfig(JSON.stringify({ ...valid, injection: { max_topics: 4, max_tokens: 1200 } }))._tag).toBe(
"None",
)
expect(MemorySchema.MIN_TOPIC_LIMIT).toBe(10)
})
})
1 change: 0 additions & 1 deletion packages/opencode/test/memory/memory-admission.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ const config = {
enabled: true,
model: "test/memory-small",
topic_limit: 10,
topic_limit_floor: 10,
turn_interval: 5,
injection: { max_topics: 3, max_tokens: 1_200 },
} as const
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ const baseConfig = {
enabled: true,
model: "test/memory-on",
topic_limit: 10,
topic_limit_floor: 10,
turn_interval: 5,
injection: { max_topics: 3, max_tokens: 1_200 },
} satisfies MemorySchema.Config
Expand Down
1 change: 0 additions & 1 deletion packages/opencode/test/memory/memory-persistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const config = {
enabled: true,
model: "test/memory-small",
topic_limit: 10,
topic_limit_floor: 10,
turn_interval: 5,
injection: { max_topics: 3, max_tokens: 1_200 },
} satisfies MemorySchema.Config
Expand Down
19 changes: 6 additions & 13 deletions packages/opencode/test/memory/memory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const config = {
enabled: true,
model: "test/memory-small",
topic_limit: 10,
topic_limit_floor: 10,
turn_interval: 5,
injection: { max_topics: 3, max_tokens: 1_200 },
} satisfies MemorySchema.Config
Expand Down Expand Up @@ -454,26 +453,29 @@ describe("memory config and YAML store", () => {
)
expect(yield* memoryConfig.load(tmp)).toBeUndefined()

// topic_limit_floor was removed — legacy files still carrying it decode
// normally (extra property tolerated); the decoded config never has it,
// and load never rewrites the file.
yield* Effect.promise(() =>
fs.writeFile(path.join(directory, "memory.jsonc"), JSON.stringify({ ...config, topic_limit_floor: 50 })),
)
expect(yield* memoryConfig.load(tmp)).toBeUndefined()
expect((yield* memoryConfig.load(tmp))?.config.topic_limit).toBe(10)

yield* Effect.promise(() =>
fs.writeFile(
path.join(directory, "memory.jsonc"),
JSON.stringify({ ...config, topic_limit: 50, topic_limit_floor: 10 }),
),
)
expect((yield* memoryConfig.load(tmp))?.config).toMatchObject({ topic_limit: 50, topic_limit_floor: 50 })
expect((yield* memoryConfig.load(tmp))?.config).toMatchObject({ topic_limit: 50 })

yield* Effect.promise(() =>
fs.writeFile(
path.join(directory, "memory.jsonc"),
JSON.stringify({ ...config, topic_limit: 20, topic_limit_floor: 50 }),
),
)
expect(yield* memoryConfig.load(tmp)).toBeUndefined()
expect((yield* memoryConfig.load(tmp))?.config.topic_limit).toBe(20)
}),
)

Expand Down Expand Up @@ -1392,7 +1394,6 @@ describe("memory bootstrap", () => {
enabled: true,
model: "test/small",
topic_limit: 10,
topic_limit_floor: 10,
turn_interval: 5,
injection: { max_topics: 3, max_tokens: 1_200 },
})
Expand Down Expand Up @@ -1446,7 +1447,6 @@ describe("memory bootstrap", () => {
...config,
model: "test/conversation",
topic_limit: 42,
topic_limit_floor: 42,
turn_interval: 9,
}
const memory = yield* Memory.Service
Expand All @@ -1457,7 +1457,6 @@ describe("memory bootstrap", () => {
expect(bootstrap.state.global).toMatchObject({
model: "test/conversation",
topic_limit: 42,
topic_limit_floor: 42,
turn_interval: 9,
})
expect(bootstrap.state.modelCalls).toBe(0)
Expand All @@ -1475,7 +1474,6 @@ describe("memory bootstrap", () => {
...config,
model: "removed/model",
topic_limit: 37,
topic_limit_floor: 37,
turn_interval: 7,
}
const memory = yield* Memory.Service
Expand All @@ -1485,7 +1483,6 @@ describe("memory bootstrap", () => {
expect(bootstrap.state.written).toMatchObject({
model: "test/compaction",
topic_limit: 37,
topic_limit_floor: 37,
turn_interval: 7,
})
expect(bootstrap.state.modelCalls).toBe(0)
Expand Down Expand Up @@ -1523,7 +1520,6 @@ describe("memory bootstrap", () => {
expect(bootstrap.state.written).toMatchObject({
model: "test/conversation",
topic_limit: 10,
topic_limit_floor: 10,
turn_interval: 5,
})
expect(bootstrap.state.modelCalls).toBe(0)
Expand All @@ -1544,7 +1540,6 @@ describe("memory bootstrap", () => {
...config,
model: "removed/model",
topic_limit: 37,
topic_limit_floor: 37,
turn_interval: 7,
}
const memory = yield* Memory.Service
Expand All @@ -1561,7 +1556,6 @@ describe("memory bootstrap", () => {
expect(bootstrap.state.written).toMatchObject({
model: "test/conversation",
topic_limit: 37,
topic_limit_floor: 37,
turn_interval: 7,
})
expect(bootstrap.state.modelCalls).toBe(0)
Expand Down Expand Up @@ -1601,7 +1595,6 @@ describe("memory bootstrap", () => {
expect(bootstrap.state.written).toMatchObject({
model: "test/conversation",
topic_limit: 10,
topic_limit_floor: 10,
turn_interval: 5,
})
expect(bootstrap.state.modelCalls).toBe(0)
Expand Down
1 change: 0 additions & 1 deletion packages/opencode/test/project/worktree-remove.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ describe("Worktree.remove", () => {
enabled,
model,
topic_limit: 10,
topic_limit_floor: 10,
turn_interval: 5,
injection: { max_topics: 3, max_tokens: 1_200 },
})
Expand Down
Loading