sage-mem 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bowen Zheng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # sage-mem · DSH 的记忆系统插件
2
+
3
+ 给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)装上**跨会话记忆**——agent 今天记住的事,明天的新会话自动想起来。
4
+
5
+ ## 为什么需要它
6
+
7
+ DSH 原生没有记忆系统。每个会话都是白纸:用户说过「我喜欢在深夜写代码」,下个会话再问「我喜欢什么」,agent 只能答不知道。
8
+
9
+ sage-mem 补上这一块。它监听 DSH 的事件流,把会话内容沉淀到 SQLite,新会话启动时自动检索注入历史记忆。
10
+
11
+ **中文优先**。市面上的记忆方案在中文检索上普遍失效(SQLite FTS5 默认分词器对中文全部 0 命中)。sage-mem 的 worker 用 trigram 分词 + 短词兜底,中文实测全命中。
12
+
13
+ ## 功能
14
+
15
+ - **自动捕获**:工具调用(读写文件、执行命令)自动记录,无需手动整理
16
+ - **AI 压缩**:会话内容由 LLM 压缩成结构化记忆(事实、摘要),中文输出
17
+ - **跨会话注入**:新会话启动时自动检索并注入历史记忆,agent 不用自己翻
18
+ - **中文检索**:trigram 分词(≥3 字查询)+ LIKE 兜底(<3 字短词如「猫」「芝麻」)
19
+
20
+ ## 架构
21
+
22
+ ```
23
+ DSH(Cordis 插件)
24
+ └─ sage-mem 插件(事件监听 → HTTP 转发)← 本仓库
25
+ │ HTTP 127.0.0.1
26
+ ▼
27
+ sage-mem worker(Bun 常驻,fork 自 claude-mem)← gezi-wen/claude-mem
28
+ │
29
+ ▼
30
+ SQLite(FTS5 trigram 中文分词 + LLM 压缩)
31
+ ```
32
+
33
+ 设计选择:记忆逻辑复用 [claude-mem](https://github.com/thedotmack/claude-mem)(Apache-2.0,9 万星),fork 后加中文修复,DSH 侧只写一个轻量桥接插件。改动最小,能追上游更新。
34
+
35
+ ## 安装
36
+
37
+ ### 1. 部署 worker
38
+
39
+ ```bash
40
+ git clone https://github.com/gezi-wen/claude-mem.git
41
+ cd claude-mem
42
+ git checkout sage-mem-worker # 含中文 trigram 修复
43
+ bun install
44
+ ```
45
+
46
+ 配置 `~/.claude-mem/settings.json`(worker 数据目录):
47
+
48
+ ```json
49
+ {
50
+ "CLAUDE_MEM_PROVIDER": "openrouter",
51
+ "CLAUDE_MEM_OPENROUTER_BASE_URL": "https://api.deepseek.com",
52
+ "CLAUDE_MEM_OPENROUTER_MODEL": "deepseek-chat",
53
+ "CLAUDE_MEM_CHROMA_ENABLED": "false",
54
+ "CLAUDE_MEM_MODE": "code--zh"
55
+ }
56
+ ```
57
+
58
+ `~/.claude-mem/.env` 放 API key(任意 OpenAI 兼容服务均可,不限于 DeepSeek):
59
+
60
+ ```
61
+ OPENROUTER_API_KEY=<你的 API key>
62
+ ```
63
+
64
+ 启动:
65
+
66
+ ```bash
67
+ bun src/services/worker-service.ts --daemon
68
+ ```
69
+
70
+ ### 2. 安装插件
71
+
72
+ 在你的 DSH profile 的 `package.json` 里加:
73
+
74
+ ```json
75
+ {
76
+ "dependencies": {
77
+ "sage-mem": "github:gezi-wen/sage-mem"
78
+ },
79
+ "dsh": {
80
+ "profile": {
81
+ "bundles": ["sage-mem"]
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ 或 clone 后本地 link:
88
+
89
+ ```json
90
+ {
91
+ "dependencies": {
92
+ "sage-mem": "link:../sage-mem"
93
+ }
94
+ }
95
+ ```
96
+
97
+ 然后 `pnpm install` 重启 DSH。
98
+
99
+ ## 验证
100
+
101
+ 跨会话中文记忆链路(实测):
102
+
103
+ 1. 会话 A 说「记住:我的猫叫芝麻,它喜欢晒太阳」
104
+ 2. 开新会话 B 问「我的猫叫什么」
105
+ 3. agent 直接答出「芝麻」——记忆自动注入,无需 agent 自己找
106
+
107
+ 搭配 [sage-persona](https://github.com/gezi-wen/sage-persona)(我是谁)和 [sage-meta](https://github.com/gezi-wen/sage-meta)(我做得对吗)组成 Sage 三件套——这是三道题的第三道:**我下次应该怎么想**。
108
+
109
+ ## 已知边界
110
+
111
+ - trigram 分词对 `<3` 字符查询无效,worker 已加 LIKE 兜底
112
+ - worker 不可用时插件静默跳过(不阻塞 DSH),记忆功能暂缺
113
+ - MVP 存储统一在 `project=sage` 空间,按工作区分项目是后续计划
114
+
115
+ ## License
116
+
117
+ MIT(worker fork 为 Apache-2.0,见 [gezi-wen/claude-mem](https://github.com/gezi-wen/claude-mem))
@@ -0,0 +1,4 @@
1
+ # sage-mem bundle patch — injects the memory bridge plugin into the profile tree.
2
+ - insert:
3
+ - id: sage-mem
4
+ name: 'sage-mem'
package/lib/index.js ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * sage-mem — 记忆桥接插件。
3
+ *
4
+ * 把 DSH 的事件流翻译成 sage-mem worker(fork 自 claude-mem)的 HTTP 调用:
5
+ * user/message → POST /api/sessions/init 存用户 prompt
6
+ * tool/call + result → POST /api/sessions/observations 捕获工具 IO
7
+ * turn/end → POST /api/sessions/summarize 触发压缩
8
+ * + 拉取最新记忆上下文缓存
9
+ * agent/session-start → 同步注入上个会话的记忆缓存(避免异步注入的时序丢失)
10
+ *
11
+ * 注入用「上一回合结束时的缓存」而非当场 fetch:agent/session-start 时 turn
12
+ * 组装已经开始,异步 fetch 回来再 inject 会被 inbox 消费时序吞掉。缓存在上个
13
+ * turn/end 时已就绪,session-start 同步注入必然生效。
14
+ *
15
+ * worker 不可用时静默跳过,不阻塞 DSH 主流程。worker 进程由启动脚本管理
16
+ * (sage-bundle 阶段统一装配,先起 worker 再起 dsh)。
17
+ *
18
+ * 这是 Sage 的第三道题:我下次应该怎么想 —— 跨会话沉淀事实与教训。
19
+ */
20
+
21
+ const WORKER_URL = process.env.SAGE_MEM_WORKER_URL || 'http://127.0.0.1:37700'
22
+ const REQUEST_TIMEOUT_MS = 3000
23
+ const PROJECT = 'sage' // MVP:统一记忆空间;后续可按 cwd 分项目
24
+
25
+ export const name = 'sage-mem'
26
+ export const inject = []
27
+
28
+ /**
29
+ * 构造 UserMessage(手写,零依赖:dsh-llm 无法从 workspace 插件解析)。
30
+ * source 必需——DSH 消费消息时访问 message.source.kind,纯字符串会崩。
31
+ */
32
+ function pluginMessage(text, plugin) {
33
+ return {
34
+ id: crypto.randomUUID(),
35
+ role: 'user',
36
+ content: [{ type: 'text', text }],
37
+ source: { kind: 'plugin', plugin },
38
+ }
39
+ }
40
+
41
+ export function apply(ctx) {
42
+ // tool/call 先记 callId,等 tool/result 配对后一起送 worker
43
+ const pendingCalls = new Map()
44
+ // 上个会话的记忆上下文(turn/end 时拉取,下个 session-start 同步注入)
45
+ let cachedContext = ''
46
+ // 最后一条 assistant 消息(summarize 需要)
47
+ let lastAssistantText = ''
48
+
49
+ async function post(path, body) {
50
+ try {
51
+ return await fetch(WORKER_URL + path, {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/json' },
54
+ body: JSON.stringify(body),
55
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
56
+ })
57
+ } catch {
58
+ return null // worker 不可用:静默跳过,不阻塞 DSH
59
+ }
60
+ }
61
+
62
+ function textOf(message) {
63
+ const blocks = Array.isArray(message?.content) ? message.content : []
64
+ const texts = blocks.filter(b => b?.type === 'text').map(b => b.text).filter(Boolean)
65
+ return texts.length ? texts.join('\n') : null
66
+ }
67
+
68
+ // 1. 会话启动:同步注入上个会话的记忆缓存
69
+ // 注意:事件名是 'agent/session-start'('emit' 只是分发模式,不是事件名)
70
+ ctx.on('agent/session-start', (payload) => {
71
+ const { agent } = payload
72
+ if (cachedContext) {
73
+ agent.inject(pluginMessage(`以下是过往会话沉淀的记忆(sage-mem 检索):\n\n${cachedContext}`, 'sage-mem'))
74
+ }
75
+ })
76
+
77
+ // 2. 会话内事件:写入记忆
78
+ ctx.on('session/event', (subject, event) => {
79
+ const sessionId = subject?.header?.id
80
+ if (!sessionId) return
81
+
82
+ switch (event.type) {
83
+ case 'user/message': {
84
+ const text = textOf(event.data)
85
+ if (!text) return
86
+ post('/api/sessions/init', { contentSessionId: sessionId, project: PROJECT, prompt: text })
87
+ break
88
+ }
89
+ case 'assistant/message': {
90
+ // payload 是 { turn, step, message, usage },message 嵌套一层
91
+ const text = textOf(event.data?.message)
92
+ if (text) lastAssistantText = text
93
+ break
94
+ }
95
+ case 'tool/call': {
96
+ pendingCalls.set(event.data.callId, { name: event.data.name, arguments: event.data.arguments })
97
+ break
98
+ }
99
+ case 'tool/result': {
100
+ const message = event.data?.message
101
+ const call = pendingCalls.get(message?.callId)
102
+ pendingCalls.delete(message?.callId)
103
+ if (!call) return
104
+ post('/api/sessions/observations', {
105
+ contentSessionId: sessionId,
106
+ tool_name: call.name,
107
+ tool_input: call.arguments,
108
+ tool_response: message.content,
109
+ })
110
+ break
111
+ }
112
+ case 'turn/end': {
113
+ post('/api/sessions/summarize', {
114
+ contentSessionId: sessionId,
115
+ ...(lastAssistantText ? { last_assistant_message: lastAssistantText } : {}),
116
+ })
117
+ // 拉取最新记忆上下文,缓存给下一个会话注入
118
+ fetch(`${WORKER_URL}/api/context/inject?project=${encodeURIComponent(PROJECT)}`, {
119
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
120
+ })
121
+ .then(r => (r.ok ? r.text() : null))
122
+ .then(text => {
123
+ if (text) cachedContext = text
124
+ })
125
+ .catch(() => {})
126
+ break
127
+ }
128
+ }
129
+ })
130
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "sage-mem",
3
+ "description": "Sage memory plugin: bridges DSH session events to the sage-mem worker (forked claude-mem) for cross-session memory",
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./package.json": "./package.json"
10
+ },
11
+ "files": [
12
+ "lib/index.js",
13
+ "cordis.patch.yml"
14
+ ],
15
+ "license": "MIT",
16
+ "dsh": {
17
+ "bundle": {
18
+ "patch": "./cordis.patch.yml"
19
+ }
20
+ },
21
+ "peerDependencies": {
22
+ "@deepseek-ai/cordis": "^4.0.1"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/gezi-wen/sage-mem.git"
27
+ },
28
+ "keywords": [
29
+ "dsh",
30
+ "deepseek-harness",
31
+ "plugin"
32
+ ]
33
+ }