Skip to content
Open
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
.DS_Store
node_modules
.worktrees
.sst
Expand Down
17 changes: 17 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,20 @@ const table = sqliteTable("session", {
## Type Checking

- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly.

## Search & File Lookup Strategy

优先级从高到低:

1. **everything-search MCP** — 找文件路径首选,速度快、全盘索引
2. **Grep** — 搜文件内容用,支持正则
3. **Glob** — 按模式匹配文件名
4. **Read** — 已知路径直接读,不要先 ls 再 Read

### 执行规则

- **先想后做**:收到任务 → 1句话复述 → 确认理解再动手
- **并行调用**:多个独立 glob/grep/read 同一轮发出,不要串行
- **最小调用**:完成简单任务 ≤ 3轮调用,每步问"能不能合并"
- **不重复搜**:同一个地方只搜一次,记住结果
- **不绕路**:用户问"有没有X" → 查记忆 → 没有就直接说,不要翻遍所有配置
16 changes: 11 additions & 5 deletions packages/opencode/src/cli/cmd/tui/context/exit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
// Reset window title before destroying renderer
renderer.setTerminalTitle("")
renderer.destroy()
// SGR reset + show cursor + OSC 110/111/112 reset terminal fg/bg/cursor color.
// Without the OSC resets, whatever fg/bg the active mimocode theme pushed
// via OSC 10/11/12 would persist in the terminal session, leaving the
// shell prompt unreadable (e.g. white-on-white).
process.stdout.write("\x1b[0m\x1b[?25h\x1b]110\x07\x1b]111\x07\x1b]112\x07")
// Disable mouse event tracking (X10/buttons/all-motion/SGR) + SGR reset +
// show cursor + OSC 110/111/112 reset terminal fg/bg/cursor color.
// Without mouse disable, abnormal exit (e.g. killed by signal) leaves the
// terminal in mouse-tracking mode, producing garbage [row;colM sequences on
// every mouse move. Without the OSC resets, whatever fg/bg the active
// mimocode theme pushed via OSC 10/11/12 would persist in the terminal
// session, leaving the shell prompt unreadable (e.g. white-on-white).
process.stdout.write("\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[0m\x1b[?25h\x1b]110\x07\x1b]111\x07\x1b]112\x07")
win32FlushInputBuffer()
if (reason) {
const formatted = FormatError(reason) ?? FormatUnknownError(reason)
Expand All @@ -60,6 +63,9 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
},
)
process.on("SIGHUP", () => exit())
process.on("SIGINT", () => exit())
process.on("SIGTERM", () => exit())
process.on("beforeExit", () => exit())
return exit
},
})
9 changes: 8 additions & 1 deletion packages/opencode/src/cli/cmd/tui/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { withNetworkOptions, resolveNetworkOptionsNoConfig } from "@/cli/network
import { Filesystem } from "@/util"
import type { GlobalEvent } from "@mimo-ai/sdk/v2"
import type { EventSource } from "./context/sdk"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
import { win32DisableProcessedInput, win32InstallCtrlCGuard, disableMouseTracking } from "./win32"
import { writeHeapSnapshot } from "v8"
import { TuiConfig } from "./config/tui"
import { MIMOCODE_PROCESS_ROLE, MIMOCODE_RUN_ID, ensureRunID, sanitizedProcessEnv } from "@/util/mimo-process"
Expand Down Expand Up @@ -245,6 +245,12 @@ export const TuiThreadCommand = cmd({
process.on("uncaughtException", error)
process.on("unhandledRejection", error)
process.on("SIGUSR2", reload)
process.on("SIGINT", () => stop())
process.on("SIGTERM", () => stop())

// Ensure mouse tracking is disabled on any exit path (including abnormal exits).
// This prevents terminal garbage (SGR mouse coordinates) after crashes or kills.
process.on("exit", disableMouseTracking)

let stopped = false
const stop = async () => {
Expand All @@ -253,6 +259,7 @@ export const TuiThreadCommand = cmd({
process.off("uncaughtException", error)
process.off("unhandledRejection", error)
process.off("SIGUSR2", reload)
disableMouseTracking()
await withTimeout(client.call("shutdown", undefined), 5000).catch((error) => {
Log.Default.warn("worker shutdown failed", {
error: errorMessage(error),
Expand Down
21 changes: 21 additions & 0 deletions packages/opencode/src/cli/cmd/tui/win32.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@ import type { ReadStream } from "node:tty"
const STD_INPUT_HANDLE = -10
const ENABLE_PROCESSED_INPUT = 0x0001

/**
* Disable all terminal mouse tracking modes.
*
* Sends the appropriate CSI sequences to turn off X10, VT200, button-event,
* any-event and SGR extended mouse tracking. Safe to call even when mouse
* tracking was never enabled.
*/
export function disableMouseTracking() {
if (!process.stdout.isTTY) return
// X10 (normal tracking)
process.stdout.write("\x1b[?1000l")
// VT200 highlight tracking
process.stdout.write("\x1b[?1001l")
// Button-event tracking
process.stdout.write("\x1b[?1002l")
// Any-event tracking
process.stdout.write("\x1b[?1003l")
// SGR extended coordinate mode
process.stdout.write("\x1b[?1006l")
}

const kernel = () =>
dlopen("kernel32.dll", {
GetStdHandle: { args: ["i32"], returns: "ptr" },
Expand Down
7 changes: 7 additions & 0 deletions packages/opencode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { PluginCommand } from "./cli/cmd/plug"
import { Heap } from "./cli/heap"
import { drizzle } from "drizzle-orm/bun-sqlite"
import { ensureProcessMetadata } from "./util/mimo-process"
import { loadEnv, loadEnvFromMultipleDirs } from "./util/dotenv"

const processMetadata = ensureProcessMetadata("main")

Expand Down Expand Up @@ -90,6 +91,12 @@ const cli = yargs(args)
type: "boolean",
})
.middleware(async (opts) => {
loadEnvFromMultipleDirs([
process.cwd(),
Global.Path.config,
Global.Path.home,
])

if (opts.pure) {
process.env.MIMOCODE_PURE = "1"
}
Expand Down
26 changes: 22 additions & 4 deletions packages/opencode/src/memory/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Database } from "../storage"
import { Config } from "../config"
import { reconcileMemory } from "./reconcile"
import { buildFtsQuery } from "./fts-query"
import { EsMemory } from "../util/es-memory"

type SearchRow = {
path: string
Expand Down Expand Up @@ -125,12 +126,29 @@ export const layer: Layer.Layer<Service, never, Config.Service> = Layer.effect(
scope_id: r.scope_id,
type: r.type,
}))
if (mapped.length === 0) return []
// Rows are ORDER BY score (best first), so mapped[0] is the top hit.
// Always keep it; drop trailing rows below `floorRatio` of its score.
const topScore = mapped[0].score
const cutoff = floorRatio > 0 ? topScore * floorRatio : -Infinity
return mapped.filter((r, i) => i === 0 || r.score >= cutoff).slice(0, limit)
const topScore = mapped.length > 0 ? mapped[0].score : 0
const cutoff = mapped.length > 0 && floorRatio > 0 ? topScore * floorRatio : -Infinity
const ftsResults = mapped.length > 0
? mapped.filter((r, i) => i === 0 || r.score >= cutoff).slice(0, limit)
: []

const mapEsResult = (r: { content: string; score: number; memory_type: string; record_id: string; user_id?: string }) => ({
path: `es_memory://${r.record_id}`,
snippet: r.content.length > 200 ? r.content.slice(0, 200) + "..." : r.content,
score: r.score,
scope: "es_memory",
scope_id: r.user_id ?? "mimocode",
type: r.memory_type,
})

const esResponse = yield* Effect.promise(() =>
EsMemory.searchMulti(input.query, { user_ids: ["mimocode", "kilo", "crush", "micode", "xiaoke"], top_k: limit }),
)
const esResults = esResponse?.results ? esResponse.results.map(mapEsResult) : []

return [...ftsResults, ...esResults]
})

return Service.of({
Expand Down
37 changes: 37 additions & 0 deletions packages/opencode/src/session/checkpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { ActorPromptOps } from "@/tool/actor"
import type { ProviderID, ModelID } from "../provider/schema"
import PROMPT_CHECKPOINT_WRITER from "@/agent/prompt/checkpoint-writer.txt"
import { WriterCachePerf } from "@/actor/events"
import { EsMemory } from "../util/es-memory"
import {
metaDir,
checkpointPath,
Expand Down Expand Up @@ -379,6 +380,32 @@ function aggregateWriterCacheMetrics(
})
}

function indexCheckpointToEsMemory(sessionID: SessionID, projectID: ProjectID | undefined) {
return Effect.gen(function* () {
const ckptPath = checkpointPath(sessionID)
const ckptContent = yield* Effect.promise(() =>
Bun.file(ckptPath).text().catch(() => ""),
)
if (ckptContent) {
yield* Effect.promise(() =>
EsMemory.index(ckptContent, { memory_type: "checkpoint" }),
)
}

if (projectID) {
const memPath = memoryPath(projectID)
const memContent = yield* Effect.promise(() =>
Bun.file(memPath).text().catch(() => ""),
)
if (memContent) {
yield* Effect.promise(() =>
EsMemory.index(memContent, { memory_type: "project_memory" }),
)
}
}
})
}

// ---------------------------------------------------------------------------
// Service interface
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -895,6 +922,16 @@ export const layer: Layer.Layer<
),
)

// Index checkpoint + memory into es_memory (cross-tool shared memory).
// Fire-and-forget: failures are silent — es_memory unavailability must
// never block checkpoint settlement.
if (outcome.status === "success") {
yield* indexCheckpointToEsMemory(input.sessionID, projectID).pipe(
Effect.catch(() => Effect.void),
Effect.ignore,
)
}

// F40: capture pending before deleting the slot so a queued writer
// (held while writer1 was running) can fire as a fresh writer.
const pending = writers.get(input.sessionID)?.pending
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/tool/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const MemoryTool = Tool.define(
}
}
const lines = [
`Found ${results.length} match${results.length === 1 ? "" : "es"} (BM25-ranked, best first).`,
`Found ${results.length} match${results.length === 1 ? "" : "es"} (BM25 + vector search, ranked best first).`,
`A hit here is authoritative — use it even if a parallel/sibling query returned nothing.`,
`If you need the FULL body (snippets are truncated), Read the path.`,
`If you need an EXACT literal (a connection string, port, token, full command line, path) and the snippet/body only paraphrases or partially shows it, the curated memory may have dropped the precise form — query the history tool for the original message, which holds it verbatim.`,
Expand Down
9 changes: 6 additions & 3 deletions packages/opencode/src/tool/memory.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
Search session/project/global memory using BM25 over markdown
bodies. Use this to recall content the agent or writer subagent
bodies, PLUS vector/semantic search over the shared es_memory
service. Use this to recall content the agent or writer subagent
persisted previously: project memory, session checkpoints, task
narratives (under sessions/<sid>/tasks/), project notes, global
preferences.
preferences, and cross-tool shared memories indexed by other AI
agents (Kilo, Claude Code, Crush, etc.).

Memory layout: <data>/memory/<scope>/<scope_id>/<key>.md
Scopes: global | projects | sessions | cc (opt-in, see below)
Scopes: global | projects | sessions | cc (opt-in, see below) | es_memory (cross-agent vector search)
es_memory results search across ALL agents (kilo, crush, mimocode, xiaoke, micode); scope_id identifies the source agent.

QUERY GUIDELINES:
- Queries are OR'd and BM25-ranked: a document matches if it contains
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/util/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * as Archive from "./archive"
export * as Color from "./color"
export { getEnvInfo } from "./env-info"
export * as EsMemory from "./es-memory"
export * as Filesystem from "./filesystem"
export * as Keybind from "./keybind"
export * as LocalContext from "./local-context"
Expand Down