dsh-mnemon 0.5.6 → 0.5.8

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
@@ -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
@@ -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
package/lib/client.js CHANGED
@@ -543,7 +543,7 @@ window.__ModuleLoader__.load({
543
543
  "documents.size": "文件大小",
544
544
  "documents.archiveReceipt": "Mnemon 冷索引回执",
545
545
  "documents.archiveTitle": "迁入归档",
546
- "documents.archiveDescription": "先由受限的独立任务 Agent 写入可检索的 Mnemon 摘要和精确路径,成功后才移动原文。",
546
+ "documents.archiveDescription": "独立任务 Agent 先拟定摘要和目标空间;Host 校验后写入冷索引,再移动原文。需要已启用且支持精确写入与删除的记忆空间。",
547
547
  "documents.archive": "归档",
548
548
  "documents.archiveConfirm": "确认建立 Mnemon 索引并迁移这份档案?",
549
549
  "documents.archiving": "索引并迁移中…",
@@ -605,7 +605,7 @@ window.__ModuleLoader__.load({
605
605
  "documents.size": "File size",
606
606
  "documents.archiveReceipt": "Mnemon cold-index receipt",
607
607
  "documents.archiveTitle": "Move to archive",
608
- "documents.archiveDescription": "A restricted independent task Agent first stores a searchable Mnemon summary and exact path. The original moves only after that succeeds.",
608
+ "documents.archiveDescription": "An independent task Agent proposes a summary and destination. The Host validates and writes the cold index before moving the original. An active Memory Space with exact writes and safe deletion is required.",
609
609
  "documents.archive": "Archive",
610
610
  "documents.archiveConfirm": "Create the Mnemon index and archive this document?",
611
611
  "documents.archiving": "Indexing and moving…",
@@ -1460,6 +1460,9 @@ window.__ModuleLoader__.load({
1460
1460
  "status.title": "系统状态",
1461
1461
  "status.description": "聚焦 dsh-mnemon、各记忆 Provider、三层存储和当前读写目录;连接配置由 DSH 部署统一管理。",
1462
1462
  "status.nominal": "系统正常",
1463
+ "status.reviewFailed": "后台审查失败",
1464
+ "status.reviewFailedDetail": "本轮记忆检查未完成;候选内容会保留到后续符合条件的审查。",
1465
+ "status.reviewContextWindow": "请在 Mnemon 设置中选择上下文窗口足以覆盖父会话的任务模型。后台审查需要继承父会话。",
1463
1466
  "status.checkRequired": "需要检查",
1464
1467
  "status.rechecking": "检查中…",
1465
1468
  "status.recheck": "重新检查",
@@ -1982,6 +1985,9 @@ window.__ModuleLoader__.load({
1982
1985
  "status.title": "System Status",
1983
1986
  "status.description": "dsh-mnemon, memory providers, three-tier storage, and the current read/write root. DSH deployment owns connection configuration.",
1984
1987
  "status.nominal": "System nominal",
1988
+ "status.reviewFailed": "Background review failed",
1989
+ "status.reviewFailedDetail": "This memory checkpoint did not complete. Candidates remain pending for a later eligible review.",
1990
+ "status.reviewContextWindow": "Choose a task model in Mnemon settings whose context window covers the parent conversation. Background review inherits the parent conversation.",
1985
1991
  "status.checkRequired": "Check required",
1986
1992
  "status.rechecking": "Checking…",
1987
1993
  "status.recheck": "Check again",
@@ -6765,6 +6771,7 @@ window.__ModuleLoader__.load({
6765
6771
  const t = useT();
6766
6772
  const [versionsOpen, setVersionsOpen] = (0, react.useState)(false);
6767
6773
  const status = props.status;
6774
+ const reviewError = status?.lifecycle?.current?.lastError;
6768
6775
  const documents = status?.documents;
6769
6776
  const catalogKnown = status?.memoryBodies !== void 0;
6770
6777
  const memorySpaces = (0, react.useMemo)(() => status?.memoryBodies ?? [], [status]);
@@ -6781,7 +6788,7 @@ window.__ModuleLoader__.load({
6781
6788
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PageHeader, {
6782
6789
  title: t("status.title"),
6783
6790
  description: t("status.description"),
6784
- meta: status === null && props.loading ? t("common.loading") : status === null ? t("status.checkRequired") : t("status.nominal"),
6791
+ meta: status === null && props.loading ? t("common.loading") : status === null || reviewError !== void 0 ? t("status.checkRequired") : t("status.nominal"),
6785
6792
  ...props.loading ? { loadingLabel: t("status.rechecking") } : {},
6786
6793
  action: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6787
6794
  className: MnemonView_module_css_default.statusHeaderActions,
@@ -7435,6 +7442,17 @@ window.__ModuleLoader__.load({
7435
7442
  role: "alert",
7436
7443
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("header.notReady") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: statusError ?? status?.error })]
7437
7444
  }),
7445
+ status?.lifecycle?.current?.lastError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7446
+ className: MnemonView_module_css_default.alert,
7447
+ role: "alert",
7448
+ "aria-label": t("status.reviewFailed"),
7449
+ children: [
7450
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("status.reviewFailed") }),
7451
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: status.lifecycle.current.lastError }),
7452
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("status.reviewFailedDetail") }),
7453
+ /CONTEXT_WINDOW_EXCEEDED|exceed(?:s|ed)? (?:the )?(?:available )?context (?:size|window)/iu.test(status.lifecycle.current.lastError) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("status.reviewContextWindow") })
7454
+ ]
7455
+ }),
7438
7456
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7439
7457
  className: MnemonView_module_css_default.workspace,
7440
7458
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceNavigation, {
@@ -7522,6 +7540,8 @@ window.__ModuleLoader__.load({
7522
7540
  const entry = document.createElement("button");
7523
7541
  entry.type = "button";
7524
7542
  entry.dataset.dshMnemonEntry = "";
7543
+ entry.dataset.dshPlugin = "dsh-mnemon";
7544
+ entry.dataset.dshPart = "sidebar-entry";
7525
7545
  entry.className = MnemonWorkspace_module_css_default.entry ?? "";
7526
7546
  const icon = document.createElement("span");
7527
7547
  icon.className = MnemonWorkspace_module_css_default.entryIcon ?? "";