dsh-plugin-memory-share 0.3.1 → 0.4.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/README.md CHANGED
@@ -143,6 +143,8 @@ dsh plugin --profile desktop add "file:C:/path/to/dsh-plugin-MemoryShare"
143
143
  | `relevance` | `true` | 按当前会话最近发言给 workspace/session 条目打分,0 分不注入;`user` 条目永远注入 |
144
144
  | `relevanceTopK` | `4` | 筛选后最多保留几条非 user 条目 |
145
145
  | `relevanceKeepRecent` | `0` | 全都不相关时退回最近 n 条;`0` = 一条不留 |
146
+ | `bootSnapshot` | `live` | `live` 每步按最新记忆渲染;`session` 本会话首帧冻结,记忆再变也不失效前缀缓存 |
147
+ | `disabledSessions` | `[]` | 这些会话 id 不注入;也可用 `ctx.memoryShare.setSessionEnabled(id, bool)` 运行时开关 |
146
148
  | `injectInPrompt` | `true` | 关掉后只剩工具,不再被动注入 |
147
149
  | `guidance` | `true` | 是否附带那段用法指引 |
148
150
  | `refreshIntervalMs` | `30000` | 跨进程同步的轮询间隔;`0` = 只在启动/建会话时读 |
@@ -288,6 +290,15 @@ function ensureService(ctx) {
288
290
 
289
291
  ---
290
292
 
293
+ ## 状态检查
294
+
295
+ ```sh
296
+ dsh-plugin-memory-share # 人读
297
+ dsh-plugin-memory-share --json # 给脚本用
298
+ ```
299
+
300
+ 输出磁盘版本、**运行中版本**(插件启动时写的 `runtime.json`)、条目数与作用域分布。两边版本不一致时退出码为 1——用来回答「我装的和跑的是不是同一个版本」。
301
+
291
302
  ## 开发
292
303
 
293
304
  ```bash
package/bin/status.mjs ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ // dsh-plugin-memory-share 状态检查:磁盘版本 vs 运行中版本 vs 记忆库概况。
3
+ // 用法:dsh-plugin-memory-share [--json]
4
+ // 退出码:0 一致 / 1 版本不一致或库损坏 / 2 无法读取
5
+ import { readFile, stat } from 'node:fs/promises'
6
+ import { join } from 'node:path'
7
+ import { homedir } from 'node:os'
8
+
9
+ const asJson = process.argv.includes('--json')
10
+ const dataDir = process.env.MEMORY_SHARE_DIR
11
+ || join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'memory-share')
12
+
13
+ async function readJson(path) {
14
+ try {
15
+ return JSON.parse(await readFile(path, 'utf8'))
16
+ } catch {
17
+ return undefined
18
+ }
19
+ }
20
+
21
+ const pkg = await readJson(new URL('../package.json', import.meta.url))
22
+ const runtime = await readJson(join(dataDir, 'runtime.json'))
23
+ const store = await readJson(join(dataDir, 'memories.json'))
24
+
25
+ let storeStat
26
+ try {
27
+ storeStat = await stat(join(dataDir, 'memories.json'))
28
+ } catch {
29
+ storeStat = undefined
30
+ }
31
+
32
+ const entries = Array.isArray(store?.entries) ? store.entries : []
33
+ const counts = { user: 0, workspace: 0, session: 0 }
34
+ for (const entry of entries) {
35
+ if (counts[entry.scope] !== undefined) counts[entry.scope] += 1
36
+ }
37
+
38
+ const diskVersion = pkg?.version ?? 'unknown'
39
+ const runVersion = runtime?.version
40
+ const consistent = runVersion === undefined ? null : runVersion === diskVersion
41
+
42
+ const report = {
43
+ dataDir,
44
+ diskVersion,
45
+ running: runtime === undefined ? null : {
46
+ version: runVersion,
47
+ pid: runtime.pid,
48
+ startedAt: runtime.startedAt,
49
+ node: runtime.node,
50
+ },
51
+ consistent,
52
+ memories: {
53
+ total: entries.length,
54
+ counts,
55
+ bytes: storeStat?.size ?? 0,
56
+ updatedAt: storeStat?.mtime?.toISOString() ?? null,
57
+ },
58
+ }
59
+
60
+ if (asJson) {
61
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
62
+ } else {
63
+ const line = (label, value) => process.stdout.write(`${label.padEnd(14)}${value}\n`)
64
+ process.stdout.write('dsh-plugin-memory-share\n')
65
+ process.stdout.write('─'.repeat(46) + '\n')
66
+ line('数据目录', dataDir)
67
+ line('磁盘版本', diskVersion)
68
+ if (runtime === undefined) {
69
+ line('运行中版本', '(没有 runtime.json —— 插件未在此机器上启动过,或从未重启过后加载)')
70
+ } else {
71
+ line('运行中版本', `${runVersion}${consistent ? ' ✓ 一致' : ' ✗ 与磁盘不一致,需要重启 DSH'}`)
72
+ line('进程', `pid ${runtime.pid} 启动于 ${runtime.startedAt}`)
73
+ }
74
+ line('记忆条目', `${entries.length} 条 全局 ${counts.user} · 工作区 ${counts.workspace} · 会话 ${counts.session}`)
75
+ line('文件大小', `${(report.memories.bytes / 1024).toFixed(1)} KiB`)
76
+ line('最后写入', report.memories.updatedAt ?? '(还没有记忆库)')
77
+ }
78
+
79
+ process.exit(consistent === false ? 1 : 0)
package/lib/index.js CHANGED
@@ -10,6 +10,8 @@
10
10
  // 这样插件能装在任意 profile 下而不需要 DSH 的内部模块解析(与 dsh-agentmemory 的做法一致,但它多依赖了 dsh-tools)。
11
11
  // License: MIT
12
12
  import { MemoryStore, defaultDataDir, normalizeKey, SCOPES } from './store.js'
13
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
14
+ import { join } from 'node:path'
13
15
  import { renderMemoryBlock } from './render.js'
14
16
 
15
17
  export const name = 'memoryShare'
@@ -19,6 +21,8 @@ export const inject = ['systemPrompt', 'tools']
19
21
  /** 提示词段的名称与位置:100 = persona 之后、工具指引之前。 */
20
22
  const SECTION_NAME = 'memory-share'
21
23
  const SECTION_ORDER = 100
24
+ /** bootSnapshot=session 时最多冻结多少会话的块,防止无界增长。 */
25
+ const FROZEN_LIMIT = 200
22
26
 
23
27
  const DEFAULTS = {
24
28
  dataDir: '',
@@ -34,6 +38,10 @@ const DEFAULTS = {
34
38
  relevanceTopK: 4,
35
39
  /** 全都不相关时退回最近 n 条;0 = 一条都不留(严格模式)。 */
36
40
  relevanceKeepRecent: 0,
41
+ /** 'live' = 每步按最新记忆渲染;'session' = 本会话首次渲染后冻结,保护前缀缓存。 */
42
+ bootSnapshot: 'live',
43
+ /** 这些会话 id 不注入(服务方法可运行时开关)。 */
44
+ disabledSessions: [],
37
45
  guidance: true,
38
46
  refreshIntervalMs: 30000,
39
47
  }
@@ -128,11 +136,35 @@ function normalizeOptions(config) {
128
136
  options.relevance = options.relevance !== false
129
137
  options.relevanceTopK = Math.max(0, Math.floor(Number(options.relevanceTopK) || 0))
130
138
  options.relevanceKeepRecent = Math.max(0, Math.floor(Number(options.relevanceKeepRecent) || 0))
139
+ options.bootSnapshot = options.bootSnapshot === 'session' ? 'session' : 'live'
140
+ options.disabledSessions = Array.isArray(options.disabledSessions)
141
+ ? options.disabledSessions.map((id) => String(id)).filter(Boolean)
142
+ : []
131
143
  options.guidance = options.guidance !== false
132
144
  options.refreshIntervalMs = Math.max(0, Math.floor(Number(options.refreshIntervalMs) || 0))
133
145
  return options
134
146
  }
135
147
 
148
+ /**
149
+ * 写一份「运行中实例」标记,供 CLI 对比磁盘版本与进程版本。
150
+ * 失败不影响插件 —— 最多少一条诊断信息。
151
+ */
152
+ async function writeRuntimeMarker(dir) {
153
+ try {
154
+ const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'))
155
+ await mkdir(dir, { recursive: true })
156
+ await writeFile(join(dir, 'runtime.json'), JSON.stringify({
157
+ name: pkg.name,
158
+ version: pkg.version,
159
+ pid: process.pid,
160
+ startedAt: new Date().toISOString(),
161
+ node: process.version,
162
+ }, null, 2))
163
+ } catch {
164
+ /* 忽略 */
165
+ }
166
+ }
167
+
136
168
  export function apply(ctx, config = {}) {
137
169
  const options = normalizeOptions(config)
138
170
  const store = new MemoryStore({
@@ -141,6 +173,11 @@ export function apply(ctx, config = {}) {
141
173
  logger: ctx.logger,
142
174
  })
143
175
 
176
+ // 会话级开关与 boot 快照(bootSnapshot: 'session' 时按会话冻结渲染结果)
177
+ const sessionOff = new Set(options.disabledSessions)
178
+ const frozenBlocks = new Map()
179
+ void writeRuntimeMarker(options.dataDir)
180
+
144
181
  const warn = (message) => {
145
182
  try {
146
183
  (ctx.logger?.warn ?? console.warn)(`[memory-share] ${message}`)
@@ -184,6 +221,11 @@ export function apply(ctx, config = {}) {
184
221
  if (options.injectFor === 'top' && isDerivedAgent(context?.agent)) return ''
185
222
  try {
186
223
  const { sessionId, cwd } = identityOf(context?.agent)
224
+ if (sessionOff.has(sessionId)) return ''
225
+ if (options.bootSnapshot === 'session') {
226
+ const frozen = frozenBlocks.get(sessionId)
227
+ if (frozen !== undefined) return frozen
228
+ }
187
229
  const visible = store.selectFor({ cwd, sessionId })
188
230
  const queryText = options.relevance ? recentText.get(sessionId) : undefined
189
231
  const entries = queryText === undefined || queryText.trim() === ''
@@ -194,12 +236,17 @@ export function apply(ctx, config = {}) {
194
236
  topK: options.relevanceTopK,
195
237
  keepRecent: options.relevanceKeepRecent,
196
238
  })
197
- return renderMemoryBlock({
239
+ const block = renderMemoryBlock({
198
240
  entries,
199
241
  maxChars: options.maxChars,
200
242
  entryChars: options.entryChars,
201
243
  guidance: options.guidance,
202
244
  })
245
+ if (options.bootSnapshot === 'session' && sessionId !== '') {
246
+ if (frozenBlocks.size >= FROZEN_LIMIT) frozenBlocks.delete(frozenBlocks.keys().next().value)
247
+ frozenBlocks.set(sessionId, block)
248
+ }
249
+ return block
203
250
  } catch (error) {
204
251
  warn(`渲染记忆失败:${error?.message ?? error}`)
205
252
  return ''
@@ -437,6 +484,19 @@ export function apply(ctx, config = {}) {
437
484
  async forget(id) {
438
485
  return { removed: (await store.remove(id)) ? 1 : 0 }
439
486
  },
487
+ /** 会话级开关:false = 该会话不再注入;下次 assemble 生效。 */
488
+ async setSessionEnabled(sessionId, enabled) {
489
+ const id = String(sessionId ?? '')
490
+ if (id === '') throw new Error('memory-share: sessionId 不能为空')
491
+ if (enabled === false) sessionOff.add(id)
492
+ else sessionOff.delete(id)
493
+ frozenBlocks.delete(id)
494
+ return { sessionId: id, enabled: enabled !== false }
495
+ },
496
+ /** 当前被关掉的会话、以及已冻结的会话。 */
497
+ async listSessions() {
498
+ return { disabled: [...sessionOff], frozen: [...frozenBlocks.keys()] }
499
+ },
440
500
  }
441
501
  // Typert 反射绑定:网页客户端据此把这三个方法挂到 ctx.remote.memoryShare 上。
442
502
  service.typertRemote = Object.freeze({ service, serviceKey: 'memoryShare', namespace: 'memoryShare' })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-memory-share",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "dsh-plugin-MemoryShare — 跨会话共享的长期记忆:被动注入系统提示词,配 memory_save / memory_recall / memory_forget 三个工具,以及「设置 → 插件 → 记忆」管理页。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "files": [
29
29
  "lib",
30
+ "bin",
30
31
  "cordis.patch.yml",
31
32
  "README.md",
32
33
  "LICENSE"
@@ -75,5 +76,8 @@
75
76
  "@deepseek-ai/dsh-api-gateway"
76
77
  ]
77
78
  }
79
+ },
80
+ "bin": {
81
+ "dsh-plugin-memory-share": "bin/status.mjs"
78
82
  }
79
83
  }