From 937ef21690397d95b8bc18a5232c9fb592b8eed1 Mon Sep 17 00:00:00 2001 From: Bin Date: Fri, 12 Jun 2026 10:09:48 +0800 Subject: [PATCH 1/3] fix: restore terminal state on abnormal exit Add signal handlers for SIGINT, SIGTERM, and beforeExit to ensure terminal cleanup (raw mode, alternate screen buffer) happens even when the process is killed by model errors or external signals. Fixes: Terminal scrolling persists after CTRL+C exit, and session resume crashes due to corrupted terminal state. --- packages/opencode/src/cli/cmd/tui/context/exit.tsx | 3 +++ packages/opencode/src/cli/cmd/tui/thread.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/packages/opencode/src/cli/cmd/tui/context/exit.tsx b/packages/opencode/src/cli/cmd/tui/context/exit.tsx index 9724726f2..0a7766a1c 100644 --- a/packages/opencode/src/cli/cmd/tui/context/exit.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/exit.tsx @@ -60,6 +60,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 }, }) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index f8fe065a4..43ee8292e 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -245,6 +245,8 @@ export const TuiThreadCommand = cmd({ process.on("uncaughtException", error) process.on("unhandledRejection", error) process.on("SIGUSR2", reload) + process.on("SIGINT", () => stop()) + process.on("SIGTERM", () => stop()) let stopped = false const stop = async () => { From e2c7fcbbc29d482643c7e0a08da0da6058b3c11c Mon Sep 17 00:00:00 2001 From: Bin Date: Tue, 16 Jun 2026 14:24:19 +0800 Subject: [PATCH 2/3] fix: disable mouse tracking on exit to prevent terminal garbage after abnormal kill --- packages/opencode/src/cli/cmd/tui/context/exit.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/exit.tsx b/packages/opencode/src/cli/cmd/tui/context/exit.tsx index 0a7766a1c..253683d2a 100644 --- a/packages/opencode/src/cli/cmd/tui/context/exit.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/exit.tsx @@ -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) From 6891c2a8d595c05af028010e9fed43e00e01cb02 Mon Sep 17 00:00:00 2001 From: Bin Date: Wed, 1 Jul 2026 16:54:46 +0800 Subject: [PATCH 3/3] fix: disable mouse tracking on abnormal exit to prevent terminal garbage When MiMo-Code exits abnormally (crash, external kill, Ctrl+C), the terminal mouse tracking modes (X10, VT200, button-event, any-event, SGR) were not being reset. This caused the terminal to continuously output SGR mouse coordinate sequences like [555;106;49M when moving the mouse after exit. Fix by: 1. Adding a cross-platform disableMouseTracking() function that sends the appropriate CSI sequences to turn off all mouse tracking modes 2. Registering a process.on('exit') handler to ensure cleanup runs even on abnormal exits 3. Calling disableMouseTracking() in the normal shutdown path as well Fixes #1458 --- .gitignore | 1 - AGENTS.md | 17 ++++++++++ packages/opencode/src/cli/cmd/tui/thread.ts | 7 +++- packages/opencode/src/cli/cmd/tui/win32.ts | 21 ++++++++++++ packages/opencode/src/index.ts | 7 ++++ packages/opencode/src/memory/service.ts | 26 ++++++++++++--- packages/opencode/src/session/checkpoint.ts | 37 +++++++++++++++++++++ packages/opencode/src/tool/memory.ts | 2 +- packages/opencode/src/tool/memory.txt | 9 +++-- packages/opencode/src/util/index.ts | 1 + 10 files changed, 118 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 8f6ddeb46..21179c535 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -.DS_Store node_modules .worktrees .sst diff --git a/AGENTS.md b/AGENTS.md index 20adbc200..3b24d2007 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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" → 查记忆 → 没有就直接说,不要翻遍所有配置 diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 43ee8292e..4ce55d3e2 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -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" @@ -248,6 +248,10 @@ export const TuiThreadCommand = cmd({ 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 () => { if (stopped) return @@ -255,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), diff --git a/packages/opencode/src/cli/cmd/tui/win32.ts b/packages/opencode/src/cli/cmd/tui/win32.ts index 1aaa80aec..d482f2385 100644 --- a/packages/opencode/src/cli/cmd/tui/win32.ts +++ b/packages/opencode/src/cli/cmd/tui/win32.ts @@ -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" }, diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 9c3160a8e..694bb5c23 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -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") @@ -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" } diff --git a/packages/opencode/src/memory/service.ts b/packages/opencode/src/memory/service.ts index fc3a6510e..8331a58b2 100644 --- a/packages/opencode/src/memory/service.ts +++ b/packages/opencode/src/memory/service.ts @@ -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 @@ -125,12 +126,29 @@ export const layer: Layer.Layer = 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({ diff --git a/packages/opencode/src/session/checkpoint.ts b/packages/opencode/src/session/checkpoint.ts index 0a53f14f1..f5ab51079 100644 --- a/packages/opencode/src/session/checkpoint.ts +++ b/packages/opencode/src/session/checkpoint.ts @@ -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, @@ -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 // --------------------------------------------------------------------------- @@ -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 diff --git a/packages/opencode/src/tool/memory.ts b/packages/opencode/src/tool/memory.ts index a2389ff36..715afe448 100644 --- a/packages/opencode/src/tool/memory.ts +++ b/packages/opencode/src/tool/memory.ts @@ -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.`, diff --git a/packages/opencode/src/tool/memory.txt b/packages/opencode/src/tool/memory.txt index 9e5ca986c..b49e7bafd 100644 --- a/packages/opencode/src/tool/memory.txt +++ b/packages/opencode/src/tool/memory.txt @@ -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//tasks/), project notes, global -preferences. +preferences, and cross-tool shared memories indexed by other AI +agents (Kilo, Claude Code, Crush, etc.). Memory layout: /memory///.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 diff --git a/packages/opencode/src/util/index.ts b/packages/opencode/src/util/index.ts index 504c010fd..e489dcf9f 100644 --- a/packages/opencode/src/util/index.ts +++ b/packages/opencode/src/util/index.ts @@ -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"