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 +3 -1
- package/README.zh-CN.md +3 -1
- package/bin/repair-legacy-session.mjs +220 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +111 -45
- package/lib/extension-sdk.js +2 -71
- package/lib/index.js +579 -244
- package/lib/{strategy-configuration-B_DsZRQG.js → strategy-configuration-BTXIWooc.js} +70 -1
- package/lib/types/src/client/locales.d.ts +14 -0
- package/lib/types/src/core/contracts/index.d.ts +1 -1
- package/lib/types/src/host/protocol.d.ts +2 -1
- package/package.json +55 -34
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
|
|
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
|
[](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
|