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/context/exit.tsx b/packages/opencode/src/cli/cmd/tui/context/exit.tsx index 9724726f2..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) @@ -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 }, }) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index f8fe065a4..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" @@ -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 () => { @@ -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), 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"