dsh-mnemon 0.5.5 → 0.5.7

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
@@ -40,7 +40,7 @@ Runtime context, searchable documents and long-term evidence share a familiar Si
40
40
 
41
41
  A **memory space** is one named, Provider-backed scope for long-term evidence. It contains individual memories and can be activated independently. The Chinese product term is **记忆空间**.
42
42
 
43
- Use the same data from Sidebar, conversation tools or Headless. Global, workspace and custom storage scopes are explicit. Direct retrieval does not spawn a Mnemon task Agent; Agent Query, semantic writes and maintenance may use the configured model. [Workflows and costs](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/en/reference/workflows.md).
43
+ Use the same data from Sidebar, conversation tools or Headless. Global, workspace, centralized workspace and custom storage scopes are explicit. Direct retrieval does not spawn a Mnemon task Agent; Agent Query, semantic writes and maintenance may use the configured model. [Workflows and costs](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/en/reference/workflows.md).
44
44
 
45
45
  ## Start with the default installation
46
46
 
@@ -52,6 +52,8 @@ dsh plugin --profile web add dsh-mnemon
52
52
  dsh web
53
53
  ```
54
54
 
55
+ The current development baseline is DSH `0.1.5-rc.1` (npm `latest`). Existing Sessions with `source summary requires notice form` need the explicit `dsh-mnemon-repair-session --input FILE --output NEW_FILE` copy repair; see [legacy Session recovery](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/en/guides/operations.md#dsh-015-compatibility-and-legacy-session-recovery) before replacing any artifact.
56
+
55
57
  For Headless, add the same package to that profile with `dsh plugin --profile headless add dsh-mnemon`.
56
58
 
57
59
  Open **Memory System → Status**, then add a Runtime memory. Select a DSH workspace before creating Documents, even with global storage. To retain long-term facts, create a Memory Space with an explicitly selected Provider. Sidebar is the default; optional Builtin placement uses the same pages.
package/README.zh-CN.md CHANGED
@@ -40,7 +40,7 @@
40
40
 
41
41
  **记忆空间(memory space)** 是由 Provider 承载、可以独立命名和激活的长期证据范围,其中包含多条具体记忆。
42
42
 
43
- Sidebar、对话工具与 Headless 使用同一套数据。全局、工作区与自定义范围明确可选。直接检索不创建 Mnemon 任务 Agent;Agent 查询、语义写入和整理可能使用已配置的模型。[流程与调用开销](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/zh-CN/reference/workflows.md)。
43
+ Sidebar、对话工具与 Headless 使用同一套数据。全局、工作区、集中工作区与自定义范围明确可选。直接检索不创建 Mnemon 任务 Agent;Agent 查询、语义写入和整理可能使用已配置的模型。[流程与调用开销](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/zh-CN/reference/workflows.md)。
44
44
 
45
45
  ## 从默认组合开始
46
46
 
@@ -58,6 +58,8 @@ Headless 使用同一个包:`dsh plugin --profile headless add dsh-mnemon`。
58
58
 
59
59
  从 v0.4 升级保留熟悉的配置、数据与工作流。三个可选增强仅在**设置 → 记忆系统**中透出,不增加 View 页或通用记忆插件管理器。[升级清单](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/zh-CN/reference/compatibility.md)。
60
60
 
61
+ 当前开发基线为 npm `latest` 发布的 DSH `0.1.5-rc.1`。现有会话若报 `source summary requires notice form`,需要显式执行 `dsh-mnemon-repair-session --input FILE --output NEW_FILE` 生成修复副本;替换任何文件前请阅读[旧会话恢复流程](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/zh-CN/guides/operations.md#dsh-015-兼容与旧会话恢复)。
62
+
61
63
  ## Source + Strategy → View
62
64
 
63
65
  [![来源事实经策略组合与核心校验,形成交给 DSH 宿主的唯一上下文视图](https://raw.githubusercontent.com/omdsh-dev/dsh-mnemon/main/docs/assets/diagrams/zh-CN/composable-memory.png)](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/zh-CN/development/architecture.md)
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto'
3
+ import { createReadStream, realpathSync } from 'node:fs'
4
+ import { link, mkdtemp, open, rm } from 'node:fs/promises'
5
+ import { dirname, join, resolve } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+ import * as zlib from 'node:zlib'
8
+
9
+ const maximumBytes = 128 * 1024 * 1024
10
+ const summaries = {
11
+ recall: 'Memory View snapshot',
12
+ instructions: 'Optional memory recall and remember reminder',
13
+ }
14
+ const sha256 = value => createHash('sha256').update(value).digest('hex')
15
+
16
+ // Locate JSON object members without reserializing message content or numbers.
17
+ // JSON.parse validates each complete row before this scanner is used.
18
+ function objectMembers(text, start) {
19
+ let cursor = start
20
+ const space = () => { while (/\s/.test(text[cursor] ?? '') && cursor < text.length) cursor++ }
21
+ const stringEnd = () => {
22
+ cursor++
23
+ while (cursor < text.length) {
24
+ if (text[cursor++] === '"') return
25
+ if (text[cursor - 1] === '\\') cursor++
26
+ }
27
+ }
28
+ const valueEnd = () => {
29
+ if (text[cursor] === '"') return stringEnd()
30
+ if (text[cursor] === '{' || text[cursor] === '[') {
31
+ let depth = 0
32
+ do {
33
+ if (text[cursor] === '"') stringEnd()
34
+ else {
35
+ if ('{['.includes(text[cursor])) depth++
36
+ if ('}]'.includes(text[cursor])) depth--
37
+ cursor++
38
+ }
39
+ } while (depth > 0)
40
+ } else {
41
+ while (cursor < text.length && !/[\s,}\]]/.test(text[cursor])) cursor++
42
+ }
43
+ }
44
+ space()
45
+ if (text[cursor++] !== '{') throw new Error('Expected a JSON object in the legacy message path.')
46
+ const members = []
47
+ const names = new Set()
48
+ space()
49
+ while (text[cursor] !== '}') {
50
+ const start = cursor
51
+ stringEnd()
52
+ const name = JSON.parse(text.slice(start, cursor))
53
+ if (names.has(name)) throw new Error('Duplicate JSON property in the legacy message path; refusing ambiguous repair.')
54
+ names.add(name)
55
+ space()
56
+ cursor++ // colon, already validated by JSON.parse
57
+ space()
58
+ const valueStart = cursor
59
+ valueEnd()
60
+ members.push({ name, start, end: cursor, valueStart })
61
+ space()
62
+ if (text[cursor] === '}') break
63
+ cursor++ // comma
64
+ space()
65
+ }
66
+ return members
67
+ }
68
+
69
+ function removeSummary(line) {
70
+ const data = objectMembers(line, 0).find(member => member.name === 'data')
71
+ const source = objectMembers(line, data.valueStart).find(member => member.name === 'source')
72
+ const members = objectMembers(line, source.valueStart)
73
+ const index = members.findIndex(member => member.name === 'summary')
74
+ const member = members[index]
75
+ const start = index === members.length - 1 && index > 0 ? members[index - 1].end : member.start
76
+ const end = index < members.length - 1 ? members[index + 1].start : member.end
77
+ return line.slice(0, start) + line.slice(end)
78
+ }
79
+
80
+ async function readBounded(path) {
81
+ const chunks = []
82
+ let length = 0
83
+ for await (const chunk of createReadStream(path)) {
84
+ length += chunk.length
85
+ if (length > maximumBytes) throw new Error('Session exceeds the 128 MiB repair limit.')
86
+ chunks.push(chunk)
87
+ }
88
+ return Buffer.concat(chunks)
89
+ }
90
+
91
+ // RFC 8878 framing: reject torn headers, blocks and checksums before Node's
92
+ // decoder (which can otherwise accept an unfinished frame) sees the bytes.
93
+ function frameLength(input, start) {
94
+ const requireBytes = end => { if (end > input.length) throw new Error('Truncated Zstandard frame; no output written.') }
95
+ requireBytes(start + 5)
96
+ if (input.readUInt32LE(start) !== 0xfd2fb528) throw new Error('Unsupported Zstandard frame; only ordinary DSH frames are accepted.')
97
+ const descriptor = input[start + 4]
98
+ if (descriptor & 0x18) throw new Error('Reserved Zstandard frame header bits are set.')
99
+ const single = (descriptor & 0x20) !== 0
100
+ const sizeFlag = descriptor >>> 6
101
+ const contentSizeBytes = sizeFlag === 0 ? (single ? 1 : 0) : [0, 2, 4, 8][sizeFlag]
102
+ let offset = start + 5 + (single ? 0 : 1) + [0, 1, 2, 4][descriptor & 3] + contentSizeBytes
103
+ requireBytes(offset)
104
+ let last = false
105
+ while (!last) {
106
+ requireBytes(offset + 3)
107
+ const block = input.readUIntLE(offset, 3)
108
+ last = (block & 1) !== 0
109
+ const type = (block >>> 1) & 3
110
+ if (type === 3) throw new Error('Reserved Zstandard block type.')
111
+ offset += 3 + (type === 1 ? 1 : block >>> 3)
112
+ requireBytes(offset)
113
+ }
114
+ offset += descriptor & 4 ? 4 : 0
115
+ requireBytes(offset)
116
+ return offset - start
117
+ }
118
+
119
+ function decodeFrames(input) {
120
+ const chunks = []
121
+ let offset = 0
122
+ let length = 0
123
+ while (offset < input.length) {
124
+ const lengthOnDisk = frameLength(input, offset)
125
+ const { buffer, engine } = zlib.zstdDecompressSync(input.subarray(offset, offset + lengthOnDisk), {
126
+ info: true, maxOutputLength: maximumBytes - length,
127
+ })
128
+ if (engine.bytesWritten !== lengthOnDisk) throw new Error('Invalid Zstandard frame length.')
129
+ offset += engine.bytesWritten
130
+ length += buffer.length
131
+ chunks.push(buffer)
132
+ }
133
+ return Buffer.concat(chunks)
134
+ }
135
+
136
+ function encodeFrames(text) {
137
+ // DSH requires the first frame to contain exactly its header line.
138
+ const end = text.indexOf('\n') + 1
139
+ const options = { params: { [zlib.constants.ZSTD_c_checksumFlag]: 1 } }
140
+ return Buffer.concat([text.slice(0, end), text.slice(end)].filter(Boolean).map(part => zlib.zstdCompressSync(part, options)))
141
+ }
142
+
143
+ export async function repairLegacySession(input, output) {
144
+ const original = await readBounded(input)
145
+ const compressed = original.subarray(0, 4).equals(Buffer.from([0x28, 0xb5, 0x2f, 0xfd]))
146
+ if (compressed && (typeof zlib.zstdDecompressSync !== 'function' || typeof zlib.zstdCompressSync !== 'function')) {
147
+ throw new Error('Zstandard sessions require Node 22.19 or newer (or Node 24+).')
148
+ }
149
+ const decoded = compressed ? decodeFrames(original) : original
150
+ const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(decoded)
151
+ if (!text.endsWith('\n')) throw new Error('Session must end with a complete newline-terminated JSON record.')
152
+ const lines = text.match(/[^\n]*\n/g) ?? []
153
+ let changes = 0
154
+ const repaired = lines.map((line, index) => {
155
+ let row
156
+ try { row = JSON.parse(line) } catch { throw new Error(`Invalid JSON at line ${index + 1}; no output written.`) }
157
+ if (index === 0) {
158
+ objectMembers(line, 0)
159
+ if (row.type !== 'session' || row.version !== 0 || typeof row.id !== 'string' || !row.id ||
160
+ !Number.isSafeInteger(row.createdAt) || row.createdAt < 0 ||
161
+ !Number.isSafeInteger(row.delegationDepth) || row.delegationDepth < 0) {
162
+ throw new Error('Only a legacy DSH format v0 Session header is supported.')
163
+ }
164
+ return line
165
+ }
166
+ const source = row?.data?.source
167
+ if (row?.type !== 'user/message' || source?.kind !== 'plugin' || source.plugin !== 'dsh-mnemon' ||
168
+ !Object.hasOwn(summaries, source.form) || source.summary !== summaries[source.form]) return line
169
+ changes++
170
+ return removeSummary(line)
171
+ }).join('')
172
+ const plain = Buffer.from(repaired)
173
+ // A clean artifact stays byte-identical, including its existing frame layout.
174
+ const result = changes === 0 ? original : compressed ? encodeFrames(repaired) : plain
175
+ if (output !== undefined) {
176
+ if (resolve(input) === resolve(output)) throw new Error('Output must be a new copy; the input cannot be overwritten.')
177
+ const temporary = await mkdtemp(join(dirname(resolve(output)), '.mnemon-repair-'))
178
+ try {
179
+ const path = join(temporary, 'session')
180
+ const file = await open(path, 'wx', 0o600)
181
+ try { await file.writeFile(result); await file.sync() } finally { await file.close() }
182
+ // Publish exclusively, including when output is a symlink or hard link.
183
+ await link(path, output)
184
+ } finally { await rm(temporary, { recursive: true, force: true }) }
185
+ }
186
+ return {
187
+ mode: output === undefined ? 'preview' : 'copy',
188
+ format: 0,
189
+ encoding: compressed ? 'zstd' : 'jsonl',
190
+ repairedMessages: changes,
191
+ inputSha256: sha256(original),
192
+ outputSha256: sha256(result),
193
+ originalPreserved: true,
194
+ }
195
+ }
196
+
197
+ async function main(args) {
198
+ const usage = 'Usage: dsh-mnemon-repair-session --input FILE [--output NEW_FILE]\nWithout --output, only preview the two known Mnemon v0 metadata repairs. Stop DSH and work on a backup copy. The input and existing output files are never overwritten.'
199
+ if (args.length === 1 && args[0] === '--help') { console.log(usage); return }
200
+ const options = new Map()
201
+ for (let index = 0; index < args.length; index += 2) {
202
+ const name = args[index]
203
+ const value = args[index + 1]
204
+ if (!['--input', '--output'].includes(name) || !value || value.startsWith('--') || options.has(name)) throw new Error(usage)
205
+ options.set(name, value)
206
+ }
207
+ if (!options.has('--input')) throw new Error(usage)
208
+ console.log(JSON.stringify(await repairLegacySession(options.get('--input'), options.get('--output')), null, 2))
209
+ }
210
+
211
+ // npm global installs expose this executable through a symlink. Resolve both
212
+ // sides while keeping imports from stdin or another entry point side-effect free.
213
+ function isEntrypoint() {
214
+ try { return process.argv[1] !== undefined && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)) }
215
+ catch { return false }
216
+ }
217
+
218
+ if (isEntrypoint()) {
219
+ main(process.argv.slice(2)).catch(error => { console.error(error.message); process.exitCode = 1 })
220
+ }
package/cordis.patch.yml CHANGED
@@ -1,6 +1,11 @@
1
1
  # Mount the Mnemon bridge into a DSH profile. A profile's final patch may
2
2
  # replace this config row as a whole; use user settings or environment
3
3
  # variables for one-field overrides such as cliPath.
4
+ # Connection's generic RPC registry uses its owning Context to mount routes.
5
+ # DSH 0.1.5's Web profile omits this dependency from that service's scope.
6
+ - id: connection
7
+ inject: [webRuntime, webServer]
8
+
4
9
  - insert:
5
10
  # Keep the v0.4 `mnemon` Entry as the public configuration target while
6
11
  # making every v0.5 component inherit its enabled state. The expression