@yolk_vat-y/dsh-project-memory 0.1.3 → 0.1.4
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/CHANGELOG.md +10 -1
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/package.json +1 -1
- package/src/link.js +17 -9
- package/src/store.js +14 -1
- package/src/tools/watch-repo.js +17 -3
- package/src/watch.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.1.4 (2026-08-25)
|
|
4
|
+
|
|
5
|
+
### 修复
|
|
6
|
+
- watch 轮询改用每轮从磁盘重载的存储快照:此前 `addRoot` 时加载的内存副本永不刷新,外部(`index_doc` / lazy)新索引的条目既会被重复 LLM 摘要,又会在轮询保存时被旧快照整体覆盖丢失
|
|
7
|
+
- `watch_repo` 现在同时把目标 root 记入会话 cwd 的 store watchlist:此前非 cwd 项目的 watch 重启插件后不会恢复;停止 watch 时同步从两侧移除
|
|
8
|
+
- 损坏的存储 JSON 不再静默当空处理:解析失败时坏文件改名备份为 `*.{时间戳}.corrupt` 并输出错误日志后再空启动,避免下次写入覆盖原始数据
|
|
9
|
+
- doc↔symbol 链接对纯拉丁符号名启用词边界匹配:符号名 `run` 不再命中文档里的 `runtime` / `runner`
|
|
4
10
|
|
|
5
11
|
### 新增
|
|
6
12
|
- 发布 npm 包 `@yolk_vat-y/dsh-project-memory`,支持 `dsh plugin --profile web add @yolk_vat-y/dsh-project-memory -w` 直接安装;`publishConfig.access` 设为 `public`(scoped 包默认 restricted,不设置会导致他人安装 403)
|
|
7
13
|
|
|
14
|
+
### 测试
|
|
15
|
+
- 测试从 101 项增至 110 项:watch 外部写入不重索引、外部条目在轮询保存后存活、损坏备份、链接词边界、watch 会话镜像增删
|
|
16
|
+
|
|
8
17
|
## 0.1.3 (2026-08-23)
|
|
9
18
|
|
|
10
19
|
### 修复
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/00080000/dsh-project-memory/actions/workflows/ci.yml) [](LICENSE) [](https://dsh-plugin.org/plugins/00080000/dsh-project-memory)
|
|
5
|
+
[](https://github.com/00080000/dsh-project-memory/actions/workflows/ci.yml) [](LICENSE) [](https://www.npmjs.com/package/@yolk_vat-y/dsh-project-memory) [](https://dsh-plugin.org/plugins/00080000/dsh-project-memory)
|
|
6
6
|
|
|
7
7
|
Persistent project memory for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh) agents. Indexes documents (PDF / Markdown / txt) and code symbols into a per-workspace store, refreshes them automatically, and recalls them with source citations — documents are cross-linked to the code symbols they reference.
|
|
8
8
|
|
|
@@ -52,7 +52,7 @@ dsh plugin --profile web add @yolk_vat-y/dsh-project-memory -w
|
|
|
52
52
|
A prebuilt tarball is published with each release, installable without a build step:
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
dsh plugin --profile web add /path/to/dsh-project-memory-0.1.
|
|
55
|
+
dsh plugin --profile web add /path/to/dsh-project-memory-0.1.4.tgz
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
Each indexed project has its own store at `<root>/.dsh-project-memory/`. Add it to `.gitignore` if it should not be committed.
|
|
@@ -89,7 +89,7 @@ The tools below are **invoked by the agent**, not typed by the user. In the chat
|
|
|
89
89
|
|
|
90
90
|
- **In-process locking** — store writes are serialized per memory directory within one dsh process; two dsh instances sharing a project store is last-writer-wins.
|
|
91
91
|
- **Watch poll holds the lock** — while the watcher re-indexes changed docs (LLM summarization), `remember`/`forget` queue behind it. Overlapping polls serialize on the same lock: safe, but they can pile up on very large diffs.
|
|
92
|
-
- **Silent corruption recovery** — a corrupt store JSON
|
|
92
|
+
- **Silent corruption recovery** — a corrupt store JSON falls back to empty for that file and is rebuilt on the next write; the broken file is renamed to `*.corrupt` with an error logged, but its data cannot be recovered.
|
|
93
93
|
- **Absolute source paths** — entries cite absolute paths; moving a project invalidates citations until the next re-index.
|
|
94
94
|
- **`forget` by query is eager** — keyword deletion matches at ≥0.5 token overlap and may remove several notes at once; prefer deleting by id for precision.
|
|
95
95
|
- **Cross-language recall depends on index time** — with `llmQueryExpansion` off, a Chinese-only query reaches English content through bilingual keywords captured when docs are indexed, plus doc↔symbol links. Stores indexed before v0.1.1 gain bilingual keywords as files change, or immediately via `index_repo` with `reindex: true`.
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/00080000/dsh-project-memory/actions/workflows/ci.yml) [](LICENSE) [](https://dsh-plugin.org/plugins/00080000/dsh-project-memory)
|
|
5
|
+
[](https://github.com/00080000/dsh-project-memory/actions/workflows/ci.yml) [](LICENSE) [](https://www.npmjs.com/package/@yolk_vat-y/dsh-project-memory) [](https://dsh-plugin.org/plugins/00080000/dsh-project-memory)
|
|
6
6
|
|
|
7
7
|
为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)agent 提供持久化的项目记忆。将文档(PDF / Markdown / txt)与代码符号索引进每个工作区独立的存储库,自动维护更新,召回时附源文件引用——文档自动交叉链接到其提及的代码符号。
|
|
8
8
|
|
|
@@ -52,7 +52,7 @@ dsh plugin --profile web add @yolk_vat-y/dsh-project-memory -w
|
|
|
52
52
|
每个版本会附带预构建 tarball,无需构建步骤即可安装:
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
dsh plugin --profile web add /path/to/dsh-project-memory-0.1.
|
|
55
|
+
dsh plugin --profile web add /path/to/dsh-project-memory-0.1.4.tgz
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
每个被索引的项目在 `<root>/.dsh-project-memory/` 下有独立存储。如无需入库,可加入 `.gitignore`。
|
|
@@ -89,7 +89,7 @@ dsh plugin --profile web add /path/to/dsh-project-memory-0.1.3.tgz
|
|
|
89
89
|
|
|
90
90
|
- **进程内锁** — 存储写入按记忆目录在一个 dsh 进程内串行化;两个 dsh 实例共享同一项目存储时后写覆盖先写。
|
|
91
91
|
- **watch 轮询持锁** — watcher 重索引变更文档(LLM 摘要)期间,`remember`/`forget` 会排队等待。重叠轮询靠同一把锁串行:安全,但大改动时可能堆积。
|
|
92
|
-
- **损坏静默重建** — 存储 JSON
|
|
92
|
+
- **损坏静默重建** — 存储 JSON 损坏时该文件回落为空并在下次写入时重建;坏文件会改名备份为 `*.corrupt` 并输出错误日志,但该文件内的数据无法恢复。
|
|
93
93
|
- **绝对路径引用** — 条目引用绝对路径;项目搬家后引用失效,重建索引即恢复。
|
|
94
94
|
- **`forget` 按关键词删除偏激进** — 关键词删除按 ≥0.5 token 重叠匹配,可能一次删掉多条;追求精确请用 id 删除。
|
|
95
95
|
- **跨语种召回依赖索引时** — `llmQueryExpansion` 关闭时,纯中文查询靠索引时捕获的双语 keywords 和 doc↔symbol 链接触达英文内容。v0.1.1 之前建立的索引随文件变更逐步获得双语关键词,或用 `index_repo` 的 `reindex: true` 立即重建。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yolk_vat-y/dsh-project-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Persistent project memory for dsh agents: index docs (PDF/Markdown/text) and code symbols into a searchable per-workspace store, recall them with cited sources, and keep experience entries (problems -> solutions) searchable on demand.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/link.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
const LATIN_NAME = /^[A-Za-z0-9_]+$/
|
|
2
|
+
|
|
3
|
+
function buildMatcher(name) {
|
|
4
|
+
const lower = name.toLowerCase()
|
|
5
|
+
if (!LATIN_NAME.test(name)) return { lower, re: null }
|
|
6
|
+
return { lower, re: new RegExp(`(?<![a-z0-9_$])${lower}(?![a-z0-9_$])`) }
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
export function linkEntries(store) {
|
|
2
10
|
const all = store.allEntries()
|
|
3
11
|
const symbols = all.filter((e) => e.type === 'symbol')
|
|
@@ -8,21 +16,21 @@ export function linkEntries(store) {
|
|
|
8
16
|
for (const s of symbols) {
|
|
9
17
|
const name = s.keywords[0]
|
|
10
18
|
if (!name || name.length < 3) continue
|
|
11
|
-
if (!symbolByName.has(name)) symbolByName.set(name, [])
|
|
12
|
-
symbolByName.get(name).push(s)
|
|
19
|
+
if (!symbolByName.has(name)) symbolByName.set(name, { syms: [], ...buildMatcher(name) })
|
|
20
|
+
symbolByName.get(name).syms.push(s)
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
let links = 0
|
|
16
24
|
for (const doc of docs) {
|
|
17
25
|
const linked = new Set()
|
|
18
26
|
const haystack = `${doc.title || ''} ${doc.summary || ''} ${doc.keywords ? doc.keywords.join(' ') : ''}`.toLowerCase()
|
|
19
|
-
for (const [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
for (const [, entry] of symbolByName) {
|
|
28
|
+
const hit = entry.re ? entry.re.test(haystack) : haystack.includes(entry.lower)
|
|
29
|
+
for (const s of entry.syms) {
|
|
30
|
+
if (!hit) break
|
|
31
|
+
const before = linked.size
|
|
32
|
+
linked.add(s.id)
|
|
33
|
+
if (linked.size > before) links++
|
|
26
34
|
}
|
|
27
35
|
}
|
|
28
36
|
doc.linkedSymbols = linked.size ? [...linked] : undefined
|
package/src/store.js
CHANGED
|
@@ -29,11 +29,24 @@ export async function withStoreLock(memoryDir, fn) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function loadJson(filePath, fallback) {
|
|
32
|
+
let raw
|
|
32
33
|
try {
|
|
33
|
-
|
|
34
|
+
raw = readFileSync(filePath, 'utf8')
|
|
34
35
|
} catch {
|
|
35
36
|
return fallback
|
|
36
37
|
}
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(raw)
|
|
40
|
+
} catch {
|
|
41
|
+
const backup = `${filePath}.${Date.now()}.corrupt`
|
|
42
|
+
try {
|
|
43
|
+
renameSync(filePath, backup)
|
|
44
|
+
console.error(`[dsh-project-memory] corrupted ${path.basename(filePath)} moved to ${path.basename(backup)}; starting fresh`)
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(`[dsh-project-memory] corrupted ${path.basename(filePath)} could not be backed up: ${err.message}`)
|
|
47
|
+
}
|
|
48
|
+
return fallback
|
|
49
|
+
}
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
function writeJsonAtomic(filePath, data) {
|
package/src/tools/watch-repo.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import { memoryRootFor } from '../util/fs.js'
|
|
3
|
+
import { memoryRootFor, resolveIndexRoot } from '../util/fs.js'
|
|
4
4
|
import { ProjectMemoryStore, withStoreLock } from '../store.js'
|
|
5
5
|
|
|
6
6
|
export function watchRepoTool(watchManager, config) {
|
|
@@ -25,19 +25,33 @@ export function watchRepoTool(watchManager, config) {
|
|
|
25
25
|
schema: { type: 'string' },
|
|
26
26
|
render: (_args, value) => [{ type: 'text', text: value }],
|
|
27
27
|
},
|
|
28
|
-
async execute(args) {
|
|
28
|
+
async execute(args, exec) {
|
|
29
29
|
const root = path.resolve(args.root)
|
|
30
30
|
const memoryDir = memoryRootFor(root, config.memoryDir)
|
|
31
|
-
|
|
31
|
+
const sessionRoot = resolveIndexRoot(exec)
|
|
32
|
+
const sessionMemoryDir = memoryRootFor(sessionRoot, config.memoryDir)
|
|
33
|
+
return withStoreLock(memoryDir, async () => {
|
|
32
34
|
const store = new ProjectMemoryStore(memoryDir).load()
|
|
35
|
+
const mirrorSessionWatchlist = async (present) => {
|
|
36
|
+
if (path.resolve(sessionMemoryDir) === path.resolve(memoryDir)) return
|
|
37
|
+
await withStoreLock(sessionMemoryDir, () => {
|
|
38
|
+
const sessionStore = new ProjectMemoryStore(sessionMemoryDir).load()
|
|
39
|
+
const before = sessionStore.watchlist.length
|
|
40
|
+
sessionStore.watchlist = sessionStore.watchlist.filter((r) => r !== root)
|
|
41
|
+
if (present) sessionStore.addWatch(root)
|
|
42
|
+
if (sessionStore.watchlist.length !== before || present) sessionStore.save()
|
|
43
|
+
})
|
|
44
|
+
}
|
|
33
45
|
if (args.watch === false) {
|
|
34
46
|
watchManager.removeRoot(root)
|
|
35
47
|
store.watchlist = store.watchlist.filter((r) => r !== root)
|
|
36
48
|
store.save()
|
|
49
|
+
await mirrorSessionWatchlist(false)
|
|
37
50
|
return `Stopped watching: ${root}`
|
|
38
51
|
}
|
|
39
52
|
store.addWatch(root)
|
|
40
53
|
store.save()
|
|
54
|
+
await mirrorSessionWatchlist(true)
|
|
41
55
|
watchManager.addRoot(root)
|
|
42
56
|
watchManager.start(config.watchInterval * 1000)
|
|
43
57
|
return `Watching ${root} (interval ${config.watchInterval}s). Docs/code changes will be re-indexed silently.`
|
package/src/watch.js
CHANGED
|
@@ -64,6 +64,7 @@ export class WatchManager {
|
|
|
64
64
|
async pollRoot(root, state) {
|
|
65
65
|
const memoryDir = memoryRootFor(root, this.config.memoryDir)
|
|
66
66
|
await withStoreLock(memoryDir, async () => {
|
|
67
|
+
state.store = new ProjectMemoryStore(memoryDir).load()
|
|
67
68
|
const files = walkDir(root)
|
|
68
69
|
const seen = new Set()
|
|
69
70
|
let changed = 0
|