@qltk/mnemo
v0.5.9
Published
Persistent memory layer for opencode — auto recall/extract/dream, modeled on Claude Code's memory system. File-based, four types, MEMORY.md index + topic files. TypeScript.
Readme
@qltk/mnemo
Persistent memory layer for opencode — auto recall / extract / dream, modeled on Claude Code's memory system. File-based (Markdown), four types, project-scoped. Runs as a self-contained opencode plugin (fork-based, no external scheduler).
How it works
| Function | Mechanism |
|---|---|
| Recall | plugin experimental.chat.system.transform hook injects inject.md + global/project MEMORY.md indexes into the system prompt each session |
| Extract (proactively record) | main session session.idle → fork a background agent running extract.md (scans transcript, writes topic files). Throttled: min 5 new messages + 30min interval |
| Dream (consolidate) | per-slug 24h throttle → fork dream.md (merge / dedupe / prune drift / fix index). Night window (default 18:30–08:30) + backlog catches small projects that rarely idle |
| Usage tracking (0.5.0+) | on each main-session idle, plugin observes assistant Read-tool calls (hard observation) → bumps usage_count / last_used on read topics. Dream prunes low-usage stale topics after a cold-start period |
| Fork cleanup | fork sessions tagged via session.update(title); on their own session.idle they are auto-session.deleted. Tracked in-memory (runtime) + by title (survives restart, cleans orphans) |
| Per-project scope | each session looks up its directory via session.get → derives a slug → memory isolated under projects/<slug>/ |
| Auto permission | on load, detects if permission: allow is missing and auto-injects it into opencode.jsonc (backs up to .mnemo-backup). Restart once to activate |
Extract/Dream run on a free model by default (opencode/big-pickle); the main session's model is untouched.
Install
From npm (recommended for other machines)
// ~/.config/opencode/opencode.jsonc
{
"plugin": ["@qltk/mnemo@^0.5.7"]
}On first load, the plugin auto-injects "permission": "allow" into your global opencode.jsonc (with backup). Restart once to activate. No manual config editing needed.
Local development (file://)
{
"plugin": ["file:///path/to/mnemo/src/index.ts"]
}opencode's bun runtime loads .ts directly — no build step needed during development. Edit src/*.ts → restart opencode → test.
推荐配置(dream 换大 window 模型,直接复制)
dream 整理 memory 时 context 会累积(多 topic 项目可达 40 万+ token),免费模型 big-pickle 的 window 可能不够。推荐 dream 单独用大 window 模型(如 opencode-go/deepseek-v4-flash 384k),extract 保持免费 big-pickle。
openchamber「选项(JSON)」框 或 opencode.jsonc options 直接复制:
{
"forkModelDream": { "providerID": "opencode-go", "modelID": "deepseek-v4-flash" }
}或 opencode.jsonc 完整形式:
{
"plugin": [{
"id": "@qltk/mnemo@^0.5.6",
"options": {
"forkModelDream": { "providerID": "opencode-go", "modelID": "deepseek-v4-flash" }
}
}]
}保存重载即生效。extract 还用默认 big-pickle(免费),dream 用 deepseek-v4-flash(384k window,付费但便宜)。
Plugin options(0.5.6+,动态配置,优先于 env)
opencode.jsonc 用对象形式配 plugin,可在 options 里动态配置 model——改 options 重载即生效,不用 env 不用发版:
{
"plugin": [{
"id": "file:///home/liubola/my-own-dist/mnemo/src/index.ts",
"options": {
"forkModel": { "providerID": "opencode", "modelID": "big-pickle" }, // extract(context 小,免费够)
"forkModelDream": { "providerID": "opencode-go", "modelID": "deepseek-v4-flash" }, // dream(context 大,384k window)
"forkModelFallback": ["deepseek-v4-flash-free"] // 主模型 free_tier_limit 兜底
}
}]
}| option | 类型 | 说明 |
|---|---|---|
| forkModel | {providerID, modelID} | extract 模型 |
| forkModelDream | {providerID, modelID} | dream 专用(context 大,换大 window 如 opencode-go/deepseek-v4-flash;默认同 forkModel) |
| forkModelFallback | string[] | 主模型 free_tier_limit 时 fallback 序列 |
options 优先于 env(env 优先于默认)。
Configuration (env overrides)
所有配置通过环境变量覆盖——opencode 启动前 export,或写进 systemd 服务的 Environment=。
Memory 位置
| env | default | 说明 |
|---|---|---|
| OPENCODE_MEMORY_ROOT | ~/.config/opencode/memory | 记忆根目录。所有 topic 文件、MEMORY.md、.plugin.log、.state.json 都在这下面 |
| OPENCODE_MEMORY_PROMPTS_DIR | <plugin>/../prompts | prompt 模板目录(inject/extract/dream.md)。自定义时指向别处 |
Fork agent(extract/dream 后台 agent)
| env | default | 说明 |
|---|---|---|
| OPENCODE_MEMORY_FORK_MODEL_PROVIDER | opencode | fork agent 的 provider ID |
| OPENCODE_MEMORY_FORK_MODEL_ID | big-pickle | fork agent 的 model ID(0.5.3 改回 big-pickle——dream 工具更干练 + 0.5.2 extract 改 create 后不再爆配额) |
| OPENCODE_MEMORY_FORK_MODEL_FALLBACK | deepseek-v4-flash-free | 0.5.1+ 主模型 free_tier_limit 时的 fallback 序列(逗号分隔,"provider/model" 或 modelID) |
| OPENCODE_MEMORY_FORK_STUCK_TIMEOUT_MS | 600000(10min) | 0.5.1+ fork session 卡 retry/failed 不 idle 的超时阈值,超过则查 status + 清理 + 触发 fallback |
| OPENCODE_MEMORY_FORK_MODEL_DREAM_PROVIDER | opencode | 0.5.6+ dream 专用 provider(dream context 大,可换大 window) |
| OPENCODE_MEMORY_FORK_MODEL_DREAM | big-pickle | 0.5.6+ dream 专用 modelID(默认同 forkModel;如 deepseek-v4-flash) |
fork agent 默认工具权限:read/grep/glob/bash/edit/write(硬编码于
config.ts,需改源码调整)。fork session 写 memory 到项目外需要permission: allow——plugin 首次加载会自动注入到opencode.jsonc(备份原文件到.mnemo-backup),重启生效,无需手动配。
Extract 节流
| env | default | 说明 |
|---|---|---|
| OPENCODE_MEMORY_EXTRACT_MIN_NEW_MESSAGES | 5 | 触发 extract 至少需要的新消息数。低于此 skip(消息累积不丢) |
| OPENCODE_MEMORY_EXTRACT_MIN_INTERVAL_MS | 1800000(30min) | 两次 extract 最小间隔(毫秒)。小于此 skip |
| OPENCODE_MEMORY_EXTRACT_CONTEXT_BEFORE | 6 | 0.5.2+ extract create 模式下,cursor 前滑动窗口条数(消解指代) |
两个条件同时满足才 extract。skip 时不推进游标,消息累积到下次。
Dream 调度(夜间窗口)
⚠️ 0.4.0 起默认启用夜间窗口(breaking 调度变更):dream 只在 18:30–08:30(跨午夜,配置时区)触发,白天工作时间不打扰。中国机器零配置即对中国时间。
| env | default | 说明 |
|---|---|---|
| OPENCODE_MEMORY_DREAM_INTERVAL_MS | 86400000(24h) | 两次 dream 最小间隔(毫秒) |
| OPENCODE_MEMORY_DREAM_WINDOW_START | 18:30 | 窗口起点(含),HH:MM |
| OPENCODE_MEMORY_DREAM_WINDOW_END | 08:30 | 窗口终点(不含,可跨午夜),HH:MM |
| OPENCODE_MEMORY_DREAM_TZ | 进程本地时区 | IANA 时区名,如 Asia/Shanghai / UTC |
半开区间 [start, end):18:30 当刻触发、08:30:00 起算白天(skip)。 关闭窗口(全天可触发,回到 0.3.x 行为):
OPENCODE_MEMORY_DREAM_WINDOW_START=""。 非法值(25:99/Foo/Bar时区)→ 关闭窗口或回退本地时区并 warn。 时间窗 skip 时不写 lastDream 游标,每个后续 idle 重新检查,直到落入夜间窗口才触发。
Dream backlog(0.5.0+ 小项目消化)
per-slug 节流后活跃项目不再独占 dream 配额。但小项目夜间无会话 idle → 仍可能长期不 dream。backlog 机制:主 session idle 触发当前 slug dream 后,串行补跑积压 slug(全局 lock 串行)。
| env | default | 说明 |
|---|---|---|
| OPENCODE_MEMORY_DREAM_BACKLOG_DAYS | 2 | 距上次 dream 超过 N 天的 slug 视为积压,触发补跑 |
| OPENCODE_MEMORY_DREAM_BACKLOG_PER_IDLE | 1 | 每次 idle 最多补跑的 backlog slug 数(全局 lock 串行,实际并发上限 1) |
Prune 与 cold start(0.5.0+)
| env | default | 说明 |
|---|---|---|
| OPENCODE_MEMORY_PRUNE_AGE_DAYS | 30 | dream prune 年龄硬阈值:mtime 超过 N 天 + usage_count ≤ 1 → 强 prune 候选 |
| OPENCODE_MEMORY_COLD_START_DAYS | 14 | read-tracking 冷启动期:P0 上线后 N 天内 dream 跳过 usage-based prune(防老 topic 假低 usage 被误删) |
Topic 大小 / Log rotation(0.5.0+)
| env | default | 说明 |
|---|---|---|
| OPENCODE_MEMORY_TOPIC_SOFT_MAX_KB | 8 | 单 topic 文件软上限(KB),extract 超出应只留结论/拆分 |
| OPENCODE_MEMORY_LOG_MAX_BYTES | 1048576(1MB) | .plugin.log 大小阈值,超出滚动到 .plugin.log.1(单代备份) |
示例
# extract 更积极(3 条 + 10min)
export OPENCODE_MEMORY_EXTRACT_MIN_NEW_MESSAGES=3
export OPENCODE_MEMORY_EXTRACT_MIN_INTERVAL_MS=600000
# 换 fork 模型(付费,抽取质量更高)
export OPENCODE_MEMORY_FORK_MODEL_ID=deepseek-chat
# 自定义 memory 目录
export OPENCODE_MEMORY_ROOT=/data/my-memory
# dream:锁中国时间 + 收窄窗口(仅深夜 23:00-06:00)
export OPENCODE_MEMORY_DREAM_TZ=Asia/Shanghai
export OPENCODE_MEMORY_DREAM_WINDOW_START=23:00
export OPENCODE_MEMORY_DREAM_WINDOW_END=06:00
# dream:关闭夜间窗口,全天 24h 节流即可触发(回到 0.3.x 行为)
export OPENCODE_MEMORY_DREAM_WINDOW_START=设好后重启 opencode 生效。
Structure
src/ TypeScript source (bun loads .ts directly in file:// mode)
├── index.ts entry: exports { id, server } PluginModule
├── plugin.ts memoryPlugin: system.transform + event + permission.ask + config hook
├── fork-orchestrator.ts fork/create/prompt/cleanup + extract/dream throttle
├── config.ts config seam (env overrides)
├── io.ts slug.ts prompt.ts log.ts ...
prompts/ prompt templates (inject.md / extract.md / dream.md)
dist/ compiled output (npm publish), gitignoredMemory on disk (~/.config/opencode/memory/, outside this repo):
MEMORY.md global index (user type, cross-project)
<topic>.md user type topic
projects/<slug>/ per-project
MEMORY.md project index
<topic>.md project/feedback/reference type
.plugin.log debug log
.state.json dream/extract throttle stateFour types → scope
| type | scope | records | |---|---|---| | user | global | user role, cross-project preferences | | feedback | project | collaboration corrections (rule + reason + scope) | | project | project | background not derivable from code/git | | reference | project | external system links |
Design references
- Modeled on Claude Code's memory system (file-based, four types, MEMORY.md index + topic files, extract/dream fork agents)
- Project scoping borrowed from EverOs (
<app>/<project>path-encoded scope) - No RAG — relevance via LLM-judge (inject index → agent scans titles → reads topic on demand), per Claude Code / Codex
Development
See AGENTS.md for development rules and CONTEXT.md for domain vocabulary.
Publish: npm install && npm run build && npm publish.
