@yolk_vat-y/dsh-project-memory 0.5.8 → 0.5.9
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 +81 -0
- package/README.md +104 -161
- package/README.zh-CN.md +103 -160
- package/client/client.js +73 -73
- package/cordis.patch.yml +7 -1
- package/package.json +3 -2
- package/scripts/bench-synthetic.mjs +3 -3
- package/scripts/bench.mjs +1 -1
- package/src/auto-inject.js +63 -16
- package/src/client/TaskPanel.tsx +1 -1
- package/src/client/css-modules.d.ts +12 -0
- package/src/commands/insight-actions.js +2 -1
- package/src/commands/task-actions.js +6 -12
- package/src/commands/tasks.js +4 -22
- package/src/index.js +27 -1
- package/src/lazy.js +22 -80
- package/src/reflection-pipeline.js +2 -1
- package/src/setup/taskbridge.js +19 -12
- package/src/tools/forget.js +2 -2
- package/src/tools/index-doc.js +20 -5
- package/src/tools/index-repo.js +50 -17
- package/src/tools/lesson-tools.js +3 -6
- package/src/tools/query-memory.js +3 -11
- package/src/tools/remember.js +2 -2
- package/src/tools/stats.js +2 -2
- package/src/tools/task-tools.js +6 -20
- package/src/tools/watch-repo.js +17 -10
- package/src/util/fs.js +351 -16
- package/src/util/task-view.js +66 -0
- package/src/util/text.js +14 -0
- package/src/watch.js +46 -7
package/src/tools/index-repo.js
CHANGED
|
@@ -1,26 +1,45 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { statSync } from 'node:fs'
|
|
4
|
-
import { assertIndexRoot, memoryRootFor, relativePath, storeKey, walkDir } from '../util/fs.js'
|
|
4
|
+
import { assertIndexRoot, assertSafeRoot, memoryRootFor, relativePath, scanLimits, storeKey, walkDir } from '../util/fs.js'
|
|
5
5
|
import { DROP, OVERSIZE, UNCHANGED, commitFileUpdates, fileKind, planFileIndex, toFileUpdate } from '../index-pipeline.js'
|
|
6
6
|
import { ProjectMemoryStore } from '../store.js'
|
|
7
7
|
import { onFileIndexed, isTypeScriptFile } from '../enhancer.js'
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* 每批提交的文件数。旧实现把整棵树的 updates 攒到最后一次性 commit:一次全量扫描的
|
|
11
|
+
* 峰值内存 ≈ 整棵树的所有条目(家目录级别直接 OOM)。分批后峰值与树的大小解耦。
|
|
12
|
+
*/
|
|
13
|
+
const COMMIT_BATCH = 200
|
|
14
|
+
|
|
15
|
+
export async function indexRepository(ctx, config, root, { reindex = false, allowUnsafe = false } = {}) {
|
|
10
16
|
// 先校验根目录:缺了它,下面 `new ProjectMemoryStore(...).load()` 的 save() 会把
|
|
11
17
|
// 不存在的 root 连同 .dsh-project-memory 一起 mkdirSync 出来。
|
|
12
18
|
assertIndexRoot(root)
|
|
19
|
+
// 再校验安全:家目录 / 系统目录 / Homebrew 前缀整体索引会吃满内存(issue #5)。
|
|
20
|
+
assertSafeRoot(root, { allowUnsafe })
|
|
13
21
|
const memoryDir = memoryRootFor(root, config.memoryDir)
|
|
14
22
|
const store = new ProjectMemoryStore(memoryDir).load()
|
|
15
23
|
|
|
16
24
|
const seen = new Set()
|
|
17
|
-
const updates = []
|
|
18
25
|
const failures = []
|
|
26
|
+
// 增强只能在文件的基础条目**落盘之后**排队:TS 增强是「合并进已有条目」,
|
|
27
|
+
// 若先于提交跑,随后 setEntries 会把增强结果整段覆盖掉。
|
|
28
|
+
const tsFiles = []
|
|
29
|
+
let batch = []
|
|
19
30
|
let indexed = 0
|
|
20
31
|
let updated = 0
|
|
21
32
|
let skipped = 0
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
const flushBatch = () => {
|
|
35
|
+
if (!batch.length) return
|
|
36
|
+
// 中间批次不做 unseen 清理、不重建链接(最后一批统一做)——否则每批都要遍历整个 store。
|
|
37
|
+
commitFileUpdates(store, { updates: batch, link: false })
|
|
38
|
+
batch = []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { files, truncated } = walkDir(root, scanLimits(config))
|
|
42
|
+
for (const filePath of files) {
|
|
24
43
|
const rel = storeKey(relativePath(root, filePath))
|
|
25
44
|
seen.add(rel)
|
|
26
45
|
const kind = fileKind(path.extname(filePath).toLowerCase())
|
|
@@ -36,37 +55,50 @@ export async function indexRepository(ctx, config, root, { reindex = false } = {
|
|
|
36
55
|
}
|
|
37
56
|
if (plan.key === OVERSIZE) {
|
|
38
57
|
// 体积超限的代码文件:旧记录一律清掉,避免检索命中一个已经不索引的文件。
|
|
39
|
-
|
|
58
|
+
batch.push({ rel, drop: true })
|
|
40
59
|
skipped++
|
|
60
|
+
if (batch.length >= COMMIT_BATCH) flushBatch()
|
|
41
61
|
continue
|
|
42
62
|
}
|
|
43
|
-
|
|
63
|
+
batch.push(toFileUpdate(rel, plan, record))
|
|
44
64
|
if (plan.key === DROP) skipped++
|
|
45
|
-
else if (plan.type === 'code')
|
|
46
|
-
|
|
65
|
+
else if (plan.type === 'code') {
|
|
66
|
+
updated++
|
|
67
|
+
if (isTypeScriptFile(filePath)) tsFiles.push([rel, filePath])
|
|
68
|
+
} else {
|
|
69
|
+
indexed++
|
|
70
|
+
}
|
|
71
|
+
if (batch.length >= COMMIT_BATCH) flushBatch()
|
|
47
72
|
} catch {
|
|
48
73
|
failures.push(rel)
|
|
49
74
|
}
|
|
50
75
|
}
|
|
51
76
|
|
|
52
|
-
//
|
|
53
|
-
|
|
77
|
+
// 收尾:写完最后一批 + 清理本轮未见到的旧条目 + 重建链接。
|
|
78
|
+
// 截断时**不做 unseen 清理**:没扫到的文件不等于被删了。
|
|
79
|
+
const { removed } = commitFileUpdates(store, {
|
|
80
|
+
updates: batch,
|
|
81
|
+
unseen: truncated ? null : seen,
|
|
82
|
+
link: true,
|
|
83
|
+
})
|
|
54
84
|
const stats = store.stats()
|
|
55
85
|
let report =
|
|
56
86
|
`Indexed project: ${root}\n` +
|
|
57
87
|
`docs indexed: ${indexed}, code symbols updated: ${updated}, unchanged skipped: ${skipped}, removed: ${removed}\n` +
|
|
58
88
|
`memory store: ${stats.files} files, ${stats.entries} entries, ${stats.experience} experience notes`
|
|
89
|
+
if (truncated) {
|
|
90
|
+
const { maxFiles, maxDepth } = scanLimits(config)
|
|
91
|
+
report +=
|
|
92
|
+
`\nscan truncated at the safety limit (maxFiles=${maxFiles}, maxDepth=${maxDepth}): only part of the tree was indexed.` +
|
|
93
|
+
' Raise maxScanFiles/maxScanDepth if this project is legitimately that large.'
|
|
94
|
+
}
|
|
59
95
|
if (failures.length) {
|
|
60
96
|
report += `\nfailed to index ${failures.length} file(s): ${failures.join(', ')}`
|
|
61
97
|
}
|
|
62
98
|
|
|
63
|
-
// Trigger TS enhancement for code files (TS/JS only)
|
|
64
|
-
for (const
|
|
65
|
-
|
|
66
|
-
const filePath = path.join(root, update.rel)
|
|
67
|
-
if (isTypeScriptFile(filePath)) {
|
|
68
|
-
onFileIndexed(store, update.rel, filePath, config, root)
|
|
69
|
-
}
|
|
99
|
+
// Trigger TS enhancement for code files (TS/JS only), now that the base entries are on disk.
|
|
100
|
+
for (const [rel, filePath] of tsFiles) {
|
|
101
|
+
onFileIndexed(store, rel, filePath, config, root)
|
|
70
102
|
}
|
|
71
103
|
|
|
72
104
|
return report
|
|
@@ -100,6 +132,7 @@ export function indexRepoTool(ctx, config) {
|
|
|
100
132
|
// 透传原始入参:Windows 风格路径在 POSIX 上会被 resolve 成 <cwd>/D:\...,
|
|
101
133
|
// 报错时要点明这是路径风格问题,而不是“目录被删了”。
|
|
102
134
|
assertIndexRoot(root, args.root)
|
|
135
|
+
assertSafeRoot(root, { requested: args.root, allowUnsafe: config?.allowUnsafeRoots === true })
|
|
103
136
|
return indexRepository(ctx, config, root, { reindex: Boolean(args.reindex) })
|
|
104
137
|
},
|
|
105
138
|
})
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
|
-
import { assertIndexRoot, memoryRootFor,
|
|
2
|
+
import { assertIndexRoot, memoryRootFor, resolveSafeIndexRoot } from '../util/fs.js'
|
|
3
3
|
import { ProjectMemoryStore } from '../store.js'
|
|
4
4
|
import { truncate } from '../util/text.js'
|
|
5
5
|
import { cfgInsight, saveInsight, defaultGlobalFile, GlobalStore } from '../insight-store.js'
|
|
6
6
|
import { ensureGlobalInit } from '../global-seed.js'
|
|
7
7
|
import { INSIGHT_KINDS, INSIGHT_SCOPES } from '../insight-store.js'
|
|
8
|
-
|
|
9
|
-
function sessionIdOf(exec) {
|
|
10
|
-
return exec?.agent?.session?.id || exec?.ctx?.session?.id
|
|
11
|
-
}
|
|
8
|
+
import { sessionIdOf } from '../util/task-view.js'
|
|
12
9
|
|
|
13
10
|
export function lessonTool(config) {
|
|
14
11
|
const kindDesc = `insight 语义:lesson(曾踩坑/纠偏, pattern→fix) | decision(权衡选型, choice→reason) | procedure(多步指南, steps) | experience(problem→solution,v0.4 兼容)。默认 lesson。所有 kind 都可带 trigger:命中即在动手前确定性注入。`
|
|
@@ -76,7 +73,7 @@ export function lessonTool(config) {
|
|
|
76
73
|
},
|
|
77
74
|
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
|
|
78
75
|
async execute(args, exec) {
|
|
79
|
-
const root =
|
|
76
|
+
const root = resolveSafeIndexRoot(exec, args.root, config)
|
|
80
77
|
assertIndexRoot(root, args.root || root)
|
|
81
78
|
const memoryDir = memoryRootFor(root, config.memoryDir)
|
|
82
79
|
const store = new ProjectMemoryStore(memoryDir).load()
|
|
@@ -1,25 +1,17 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import { memoryRootFor,
|
|
3
|
+
import { memoryRootFor, resolveSafeIndexRoot } from '../util/fs.js'
|
|
4
4
|
import { ProjectMemoryStore, storeOverview } from '../store.js'
|
|
5
5
|
import { expandQuery } from '../llm.js'
|
|
6
6
|
import { resolveRoute } from '../llm-route.js'
|
|
7
7
|
import { GlobalStore, cfgInsight, defaultGlobalFile, recordHit } from '../insight-store.js'
|
|
8
8
|
import { recallItems } from '../recall.js'
|
|
9
9
|
import { truncate } from '../util/text.js'
|
|
10
|
+
import { stepContent, stepStatus } from '../util/task-view.js'
|
|
10
11
|
function toAbs(root, rel) {
|
|
11
12
|
return path.isAbsolute(rel) ? rel : path.join(root, rel)
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
/** 步骤在历史数据里可能是字符串或 {content|text, status}:TaskBridge/注入/反思都按 content||text 读。 */
|
|
15
|
-
function stepContent(s) {
|
|
16
|
-
if (typeof s === 'string') return s
|
|
17
|
-
return s?.content ?? s?.text ?? ''
|
|
18
|
-
}
|
|
19
|
-
function stepStatus(s) {
|
|
20
|
-
return typeof s === 'string' ? 'pending' : (s?.status || 'pending')
|
|
21
|
-
}
|
|
22
|
-
|
|
23
15
|
export function queryMemoryTool(ctx, config) {
|
|
24
16
|
return defineTool({
|
|
25
17
|
name: 'query_memory',
|
|
@@ -54,7 +46,7 @@ export function queryMemoryTool(ctx, config) {
|
|
|
54
46
|
render: (_args, value) => [{ type: 'text', text: value }],
|
|
55
47
|
},
|
|
56
48
|
async execute(args, exec) {
|
|
57
|
-
const root =
|
|
49
|
+
const root = resolveSafeIndexRoot(exec, args.root, config)
|
|
58
50
|
// 空查询不是"全部":recallItems 会过滤空串,rankEntriesStreaming 随即返回
|
|
59
51
|
// entries.slice(0, limit)(任意条目、分数 0),task 分支的 includes('') 更是命中所有任务。
|
|
60
52
|
if (!String(args.query ?? '').trim()) {
|
package/src/tools/remember.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
|
-
import { assertIndexRoot, memoryRootFor,
|
|
2
|
+
import { assertIndexRoot, memoryRootFor, resolveSafeIndexRoot } from '../util/fs.js'
|
|
3
3
|
import { ProjectMemoryStore } from '../store.js'
|
|
4
4
|
|
|
5
5
|
export function rememberTool(config) {
|
|
@@ -34,7 +34,7 @@ export function rememberTool(config) {
|
|
|
34
34
|
render: (_args, value) => [{ type: 'text', text: value }],
|
|
35
35
|
},
|
|
36
36
|
async execute(args, exec) {
|
|
37
|
-
const root =
|
|
37
|
+
const root = resolveSafeIndexRoot(exec, args.root, config)
|
|
38
38
|
// 写盘前校验根:否则一个拼错的 root 会被 mkdir 成字面量目录(连同 .dsh-project-memory)。
|
|
39
39
|
assertIndexRoot(root, args.root || root)
|
|
40
40
|
const memoryDir = memoryRootFor(root, config.memoryDir)
|
package/src/tools/stats.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
|
-
import { memoryRootFor,
|
|
2
|
+
import { memoryRootFor, resolveSafeIndexRoot } from '../util/fs.js'
|
|
3
3
|
import { ProjectMemoryStore, storeOverview } from '../store.js'
|
|
4
4
|
import { truncate } from '../util/text.js'
|
|
5
5
|
|
|
@@ -22,7 +22,7 @@ export function statsTool(config) {
|
|
|
22
22
|
render: (_args, value) => [{ type: 'text', text: value }],
|
|
23
23
|
},
|
|
24
24
|
async execute(args, exec) {
|
|
25
|
-
const root =
|
|
25
|
+
const root = resolveSafeIndexRoot(exec, args.root, config)
|
|
26
26
|
const store = new ProjectMemoryStore(memoryRootFor(root, config.memoryDir)).load()
|
|
27
27
|
const overview = storeOverview(store)
|
|
28
28
|
const files = Object.entries(store.files).sort((a, b) => String(b[1].indexedAt || '').localeCompare(String(a[1].indexedAt || '')))
|
package/src/tools/task-tools.js
CHANGED
|
@@ -1,29 +1,15 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
|
-
import { assertIndexRoot, memoryRootFor,
|
|
2
|
+
import { assertIndexRoot, memoryRootFor, resolveSafeIndexRoot } from '../util/fs.js'
|
|
3
3
|
import { ProjectMemoryStore } from '../store.js'
|
|
4
4
|
import { truncate } from '../util/text.js'
|
|
5
|
+
import { sessionIdOf, stepProgress, timeAgo } from '../util/task-view.js'
|
|
5
6
|
import { genTaskId, hash8, adoptStepsToSession, shouldAdoptToHost } from '../setup/taskbridge.js'
|
|
6
7
|
import { fireReflect } from '../reflection-pipeline.js'
|
|
7
8
|
import { resolveRoute } from '../llm-route.js'
|
|
8
9
|
import { createHash } from 'node:crypto'
|
|
9
10
|
|
|
10
|
-
function sessionIdOf(exec) {
|
|
11
|
-
return exec?.agent?.session?.id || exec?.ctx?.session?.id
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function timeAgo(iso) {
|
|
15
|
-
const diff = Date.now() - new Date(iso).getTime()
|
|
16
|
-
const m = Math.floor(diff / 60000)
|
|
17
|
-
if (m < 1) return '刚刚'
|
|
18
|
-
if (m < 60) return `${m}分钟前`
|
|
19
|
-
const h = Math.floor(m / 60)
|
|
20
|
-
if (h < 24) return `${h}小时前`
|
|
21
|
-
return `${Math.floor(h / 24)}天前`
|
|
22
|
-
}
|
|
23
|
-
|
|
24
11
|
function summaryOf(task) {
|
|
25
|
-
const done = (task
|
|
26
|
-
const total = (task.steps || []).length
|
|
12
|
+
const { done, total } = stepProgress(task)
|
|
27
13
|
const files = (task.files || []).length
|
|
28
14
|
const mark = task.archived ? ' [归档]' : ''
|
|
29
15
|
return `### ${task.title}${mark}\n- 进度: ${done}/${total} 完成\n- 文件: ${files} 个\n- 最后活动: ${timeAgo(task.lastActiveAt || task.updatedAt)}`
|
|
@@ -39,7 +25,7 @@ export function listTasksTool(config) {
|
|
|
39
25
|
},
|
|
40
26
|
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
|
|
41
27
|
async execute(args, exec) {
|
|
42
|
-
const root =
|
|
28
|
+
const root = resolveSafeIndexRoot(exec, args.root, config)
|
|
43
29
|
assertIndexRoot(root, args.root || root)
|
|
44
30
|
const store = new ProjectMemoryStore(memoryRootFor(root, config.memoryDir)).load()
|
|
45
31
|
const tasks = store.getTasks()
|
|
@@ -66,7 +52,7 @@ export function selectTaskTool(config, host) {
|
|
|
66
52
|
},
|
|
67
53
|
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
|
|
68
54
|
async execute(args, exec) {
|
|
69
|
-
const root =
|
|
55
|
+
const root = resolveSafeIndexRoot(exec, args.root, config)
|
|
70
56
|
assertIndexRoot(root, args.root || root)
|
|
71
57
|
const store = new ProjectMemoryStore(memoryRootFor(root, config.memoryDir)).load()
|
|
72
58
|
const sid = sessionIdOf(exec)
|
|
@@ -185,7 +171,7 @@ export function archiveTaskTool(config, host) {
|
|
|
185
171
|
},
|
|
186
172
|
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
|
|
187
173
|
async execute(args, exec) {
|
|
188
|
-
const root =
|
|
174
|
+
const root = resolveSafeIndexRoot(exec, args.root, config)
|
|
189
175
|
assertIndexRoot(root, args.root || root)
|
|
190
176
|
const store = new ProjectMemoryStore(memoryRootFor(root, config.memoryDir)).load()
|
|
191
177
|
const task = store.getTask(args.taskId)
|
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 { assertIndexRoot,
|
|
3
|
+
import { assertIndexRoot, assertSafeRoot, isUnsafeRoot, memoryRootFor, sessionMemoryRootOrNull } from '../util/fs.js'
|
|
4
4
|
import { ProjectMemoryStore } from '../store.js'
|
|
5
5
|
|
|
6
6
|
export function watchRepoTool(watchManager, config) {
|
|
@@ -29,23 +29,24 @@ export function watchRepoTool(watchManager, config) {
|
|
|
29
29
|
const root = path.resolve(args.root)
|
|
30
30
|
// 不存在的根不写进 watchlist:watch 每轮会 commit → save → mkdirSync,把它重新造出来。
|
|
31
31
|
// 根是**文件**时同样要拦:否则 save() 的 mkdirSync 会抛裸 ENOTDIR。
|
|
32
|
+
// 家目录 / 系统目录 / Homebrew 前缀也不监听:整体轮询扫描会吃满内存(issue #5)。
|
|
32
33
|
if (args.watch !== false) {
|
|
33
34
|
try {
|
|
34
35
|
assertIndexRoot(root, args.root)
|
|
36
|
+
assertSafeRoot(root, { requested: args.root, allowUnsafe: config?.allowUnsafeRoots === true })
|
|
35
37
|
} catch (err) {
|
|
36
38
|
return `Not watching: ${err.message}`
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
|
-
// 文件系统根 / 共享临时目录不整体监听(子目录允许):会把无关程序和测试夹具的临时文件全扫进来
|
|
40
|
-
if (args.watch !== false && isUnwatchableRoot(root)) {
|
|
41
|
-
return `Refusing to watch ${root}: it is the filesystem root or the shared temp directory. Pass a project subdirectory instead.`
|
|
42
|
-
}
|
|
43
41
|
const memoryDir = memoryRootFor(root, config.memoryDir)
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
42
|
+
// 会话记忆根与工具侧同一套解析(标记优先 → 会话 cwd);拿不到就跳过镜像。
|
|
43
|
+
const sessionRoot = sessionMemoryRootOrNull(exec, config)
|
|
44
|
+
const sessionMemoryDir = sessionRoot ? memoryRootFor(sessionRoot, config.memoryDir) : null
|
|
47
45
|
|
|
48
46
|
const mirrorSessionWatchlist = async (present) => {
|
|
47
|
+
// 会话根不可用(家目录/系统目录且无标记)时跳过镜像:那会在不该有项目记忆的目录里
|
|
48
|
+
// 造出 watch.json,而 watch.json 一旦落在那里,下次以该目录为 cwd 启动又会把它读回来。
|
|
49
|
+
if (!sessionRoot || !sessionMemoryDir) return
|
|
49
50
|
if (path.resolve(sessionMemoryDir) === path.resolve(memoryDir)) return
|
|
50
51
|
const sessionStore = new ProjectMemoryStore(sessionMemoryDir).load()
|
|
51
52
|
const removed = sessionStore.removeWatch(root)
|
|
@@ -55,11 +56,17 @@ export function watchRepoTool(watchManager, config) {
|
|
|
55
56
|
|
|
56
57
|
if (args.watch === false) {
|
|
57
58
|
watchManager.removeRoot(root)
|
|
58
|
-
store
|
|
59
|
+
// 危险根的 store 一律不读不写:load() 一个历史遗留的超大 store 光读盘就能 OOM
|
|
60
|
+
// (这正是要修的场景),而写它会在家目录里造出 .dsh-project-memory。那份
|
|
61
|
+
// watch.json 现在也没有任何启动路径会读取(restorePersisted 只认会话 cwd),
|
|
62
|
+
// 留着即可;会话侧的镜像条目照常摘掉。
|
|
63
|
+
if (!isUnsafeRoot(root)) {
|
|
64
|
+
new ProjectMemoryStore(memoryDir).load().commit((s) => s.removeWatch(root))
|
|
65
|
+
}
|
|
59
66
|
await mirrorSessionWatchlist(false)
|
|
60
67
|
return `Stopped watching: ${root}`
|
|
61
68
|
}
|
|
62
|
-
|
|
69
|
+
new ProjectMemoryStore(memoryDir).load().commit((s) => s.addWatch(root))
|
|
63
70
|
await mirrorSessionWatchlist(true)
|
|
64
71
|
watchManager.addRoot(root)
|
|
65
72
|
watchManager.start(config.watchInterval * 1000)
|