diff --git a/packages/opencode/src/memory/file.ts b/packages/opencode/src/memory/file.ts index feecf234ea29..c3a3b77423a8 100644 --- a/packages/opencode/src/memory/file.ts +++ b/packages/opencode/src/memory/file.ts @@ -1,7 +1,7 @@ import path from "path" import { Instance } from "@/project/instance" import { Log } from "@/util/log" -import type { Memory } from "./types" +import { Memory } from "./types" const log = Log.create({ service: "memory.file" }) @@ -35,6 +35,8 @@ function parseFrontmatter(raw: string): { frontmatter: Memory.Frontmatter; conte fm[line.slice(0, idx).trim()] = line.slice(idx + 1).trim() } if (!fm.topic || !fm.type) return undefined + const validTypes: readonly string[] = Memory.TYPES + if (!validTypes.includes(fm.type)) return undefined return { frontmatter: { topic: fm.topic, diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index fc52838bc977..25fb94c40074 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -19,12 +19,10 @@ import { SessionStatus } from "./status" import { SessionSummary } from "./summary" import type { Provider } from "@/provider/provider" import { Question } from "@/question" +import { detectRepetition, REPETITION_THRESHOLD } from "./repetition" export namespace SessionProcessor { const DOOM_LOOP_THRESHOLD = 3 - // 50+ consecutive repeats of a 4-200 char pattern in the last 8KB indicates a model generation loop - const REPETITION_THRESHOLD = 50 - const REPETITION_WINDOW = 8000 const log = Log.create({ service: "session.processor" }) class RepetitionError extends Error { @@ -34,24 +32,6 @@ export namespace SessionProcessor { } } - function detectRepetition(text: string): boolean { - if (text.length < REPETITION_WINDOW) return false - const tail = text.slice(-REPETITION_WINDOW) - for (let len = 4; len <= 200; len++) { - const pattern = tail.slice(-len) - let count = 0 - let pos = tail.length - len - while (pos >= 0) { - if (tail.slice(pos, pos + len) === pattern) { - count++ - pos -= len - } else break - } - if (count >= REPETITION_THRESHOLD) return true - } - return false - } - export type Result = "compact" | "stop" | "continue" export type Event = LLM.Event diff --git a/packages/opencode/src/session/repetition.ts b/packages/opencode/src/session/repetition.ts new file mode 100644 index 000000000000..6939db7216d0 --- /dev/null +++ b/packages/opencode/src/session/repetition.ts @@ -0,0 +1,21 @@ +// 50+ consecutive repeats of a 4-200 char pattern in the last 8KB indicates a model generation loop +export const REPETITION_THRESHOLD = 50 +export const REPETITION_WINDOW = 8000 + +export function detectRepetition(text: string): boolean { + if (text.length < REPETITION_WINDOW) return false + const tail = text.slice(-REPETITION_WINDOW) + for (let len = 4; len <= 200; len++) { + const pattern = tail.slice(-len) + let count = 0 + let pos = tail.length - len + while (pos >= 0) { + if (tail.slice(pos, pos + len) === pattern) { + count++ + pos -= len + } else break + } + if (count >= REPETITION_THRESHOLD) return true + } + return false +} diff --git a/packages/opencode/test/memory/file.test.ts b/packages/opencode/test/memory/file.test.ts index 95e425fdc9ca..85b866b502fb 100644 --- a/packages/opencode/test/memory/file.test.ts +++ b/packages/opencode/test/memory/file.test.ts @@ -3,6 +3,7 @@ import path from "path" import fs from "fs/promises" import { Instance } from "../../src/project/instance" import { MemoryFile } from "../../src/memory/file" +import { Memory } from "../../src/memory/types" import { tmpdir } from "../fixture/fixture" describe("memory.file", () => { @@ -193,4 +194,52 @@ describe("memory.file", () => { }, }) }) + + test.each(Memory.TYPES.map((t) => [t]))("readEntry parses valid type: %s", async (validType) => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const dir = MemoryFile.getMemoryDir() + await fs.mkdir(dir, { recursive: true }) + const raw = `---\ntopic: test\ntype: ${validType}\n---\nsome content` + await Bun.write(path.join(dir, "valid-type.md"), raw) + const read = await MemoryFile.readEntry("valid-type.md") + expect(read).toBeDefined() + expect(read!.frontmatter.type).toBe(validType) + expect(read!.frontmatter.topic).toBe("test") + expect(read!.content).toBe("some content") + }, + }) + }) + + test("readEntry returns undefined for invalid type in frontmatter", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const dir = MemoryFile.getMemoryDir() + await fs.mkdir(dir, { recursive: true }) + const raw = "---\ntopic: test\ntype: invalid-type\n---\nsome content" + await Bun.write(path.join(dir, "invalid-type.md"), raw) + const read = await MemoryFile.readEntry("invalid-type.md") + expect(read).toBeUndefined() + }, + }) + }) + + test("readEntry returns undefined for missing type in frontmatter", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const dir = MemoryFile.getMemoryDir() + await fs.mkdir(dir, { recursive: true }) + const raw = "---\ntopic: test\n---\nsome content" + await Bun.write(path.join(dir, "no-type.md"), raw) + const read = await MemoryFile.readEntry("no-type.md") + expect(read).toBeUndefined() + }, + }) + }) }) diff --git a/packages/opencode/test/session/repetition.test.ts b/packages/opencode/test/session/repetition.test.ts index b994ade8a8e1..ead1e8f44726 100644 --- a/packages/opencode/test/session/repetition.test.ts +++ b/packages/opencode/test/session/repetition.test.ts @@ -1,29 +1,6 @@ import { describe, test, expect } from "bun:test" - -// detectRepetition is a namespace-internal function in SessionProcessor. -// We replicate the algorithm here to test it in isolation. -// Constants match processor.ts: REPETITION_THRESHOLD=50, REPETITION_WINDOW=8000 - -const REPETITION_THRESHOLD = 50 -const REPETITION_WINDOW = 8000 - -function detectRepetition(text: string): boolean { - if (text.length < REPETITION_WINDOW) return false - const tail = text.slice(-REPETITION_WINDOW) - for (let len = 4; len <= 200; len++) { - const pattern = tail.slice(-len) - let count = 0 - let pos = tail.length - len - while (pos >= 0) { - if (tail.slice(pos, pos + len) === pattern) { - count++ - pos -= len - } else break - } - if (count >= REPETITION_THRESHOLD) return true - } - return false -} +import { detectRepetition } from "../../src/session/repetition" +import { REPETITION_THRESHOLD, REPETITION_WINDOW } from "../../src/session/repetition" describe("session.detectRepetition", () => { test("no repetition returns false", () => {