@modusensus/dsh-mneme 0.1.2 → 0.1.3
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 -3
- package/lib/dream.js +10 -6
- package/lib/index.js +13 -4
- package/lib/store.js +3 -2
- package/package.json +2 -2
- package/src/dream.js +10 -6
- package/src/index.js +13 -4
- package/src/store.js +3 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
6
|
-
[](https://github.com/modusensus/dsh-mneme)
|
|
7
7
|
|
|
8
8
|
> 给 DeepSeek Harness 的跨会话记忆插件:让 Agent 记住你、记住项目、自动整理记忆。**Mneme**(Μνήμη)——希腊记忆女神 Mnemosyne 之名,掌管记忆与梦境,正如 autoDream 在后台巩固记忆。
|
|
9
9
|
|
|
@@ -150,7 +150,7 @@ src/
|
|
|
150
150
|
lib/
|
|
151
151
|
├── client.js # Web 面板(手写 ModuleLoader bundle)
|
|
152
152
|
└── *.js # src 的同步分发产物
|
|
153
|
-
test/ #
|
|
153
|
+
test/ # 108 个 node:test 测试
|
|
154
154
|
```
|
|
155
155
|
|
|
156
156
|
## 🧪 开发
|
|
@@ -158,7 +158,7 @@ test/ # 106 个 node:test 测试
|
|
|
158
158
|
```bash
|
|
159
159
|
cd dsh-mneme
|
|
160
160
|
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
|
|
161
|
-
npm test # 运行
|
|
161
|
+
npm test # 运行 108 个测试(--test-isolation=none 用于受限沙箱,禁止子进程 spawn)
|
|
162
162
|
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
|
|
163
163
|
```
|
|
164
164
|
|
package/lib/dream.js
CHANGED
|
@@ -62,6 +62,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
62
62
|
let running = false;
|
|
63
63
|
let disposed = false;
|
|
64
64
|
let baseline = { count: 0, chars: 0 };
|
|
65
|
+
let inFlight = null;
|
|
65
66
|
|
|
66
67
|
function shouldTrigger(service) {
|
|
67
68
|
const memories = service.all().filter((m) => !m.archived && m.type !== "summary");
|
|
@@ -81,8 +82,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
81
82
|
running = true;
|
|
82
83
|
// Defer the onRun invocation so a synchronous throw cannot escape the
|
|
83
84
|
// timer callback (which would crash the process) and skip the teardown.
|
|
84
|
-
// Errors are logged, never swallowed silently.
|
|
85
|
-
|
|
85
|
+
// Errors are logged, never swallowed silently. inFlight lets dispose()
|
|
86
|
+
// await the running consolidation before the caller closes the store.
|
|
87
|
+
inFlight = Promise.resolve()
|
|
86
88
|
.then(() => (onRun ? onRun() : Promise.resolve({ ok: true, skipped: true })))
|
|
87
89
|
.then((result) => {
|
|
88
90
|
// Refresh the baseline only for a successful run (design §5.3: an
|
|
@@ -105,17 +107,19 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
105
107
|
})
|
|
106
108
|
.finally(() => {
|
|
107
109
|
running = false;
|
|
110
|
+
inFlight = null;
|
|
108
111
|
});
|
|
109
112
|
}, delayMs);
|
|
110
113
|
return true;
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
function dispose() {
|
|
116
|
+
async function dispose() {
|
|
114
117
|
disposed = true;
|
|
115
118
|
if (pendingTimer) { clearTimeout(pendingTimer); pendingTimer = null; }
|
|
116
|
-
// An in-flight run is left to complete naturally
|
|
117
|
-
// already paid for and aborting would discard the work.
|
|
118
|
-
//
|
|
119
|
+
// An in-flight run is left to complete naturally (its LLM calls are
|
|
120
|
+
// already paid for and aborting would discard the work). Await it so the
|
|
121
|
+
// caller can close the store only after every write has landed.
|
|
122
|
+
if (inFlight) await inFlight.catch(() => {});
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
async function runDream(ctx, service, config) {
|
package/lib/index.js
CHANGED
|
@@ -34,9 +34,15 @@ export const apply = (ctx, config) => {
|
|
|
34
34
|
const service = createService({ store, mirror, config: cfg });
|
|
35
35
|
|
|
36
36
|
// Human edits in mirror files win on every sync; merge them back first.
|
|
37
|
-
// TYPE_FILE maps each memory type to its mirror filename.
|
|
37
|
+
// TYPE_FILE maps each memory type to its mirror filename. Read every type's
|
|
38
|
+
// edits up front: mergeHumanEdits re-renders ALL mirror files on success, so
|
|
39
|
+
// a per-type read-then-merge loop would overwrite edits in files not yet read
|
|
40
|
+
// (e.g. preferences.md merging would clobber unsynced projects.md edits).
|
|
41
|
+
const humanEdits = new Map();
|
|
38
42
|
for (const type of Object.keys(TYPE_FILE)) {
|
|
39
|
-
|
|
43
|
+
humanEdits.set(type, mirror.readHumanEdits(type));
|
|
44
|
+
}
|
|
45
|
+
for (const [type, edits] of humanEdits) {
|
|
40
46
|
if (edits.length) service.mergeHumanEdits(type, edits);
|
|
41
47
|
}
|
|
42
48
|
|
|
@@ -76,11 +82,14 @@ export const apply = (ctx, config) => {
|
|
|
76
82
|
disposers.push(api.dispose);
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
// Async disposer: cordis awaits the returned promise on unload (runDisposable),
|
|
86
|
+
// so an in-flight dream run is allowed to finish before the SQLite store is
|
|
87
|
+
// closed — dream.dispose() resolves only after its current run settles.
|
|
88
|
+
return async () => {
|
|
80
89
|
for (const dispose of disposers) {
|
|
81
90
|
if (typeof dispose === "function") dispose();
|
|
82
91
|
}
|
|
83
|
-
if (dream) dream.dispose();
|
|
92
|
+
if (dream) await dream.dispose();
|
|
84
93
|
store.close();
|
|
85
94
|
};
|
|
86
95
|
};
|
package/lib/store.js
CHANGED
|
@@ -193,8 +193,9 @@ export function createStore(path) {
|
|
|
193
193
|
function search(query, { limit = 20, includeArchived = false } = {}) {
|
|
194
194
|
const q = String(query).trim();
|
|
195
195
|
if (!q) return [];
|
|
196
|
-
//
|
|
197
|
-
//
|
|
196
|
+
// Plain LIKE substring scan over title/content/tags (wildcards escaped so
|
|
197
|
+
// user input matches literally). No FTS5: CJK substring matching needs
|
|
198
|
+
// LIKE, and typical memory stores are small enough that a scan is fine.
|
|
198
199
|
const like = `%${escapeLike(q)}%`;
|
|
199
200
|
const { limit: lim } = sanitizePage(limit, 0, 20);
|
|
200
201
|
const archivedFilter = includeArchived ? "" : "archived = 0 AND ";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
|
-
"description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite
|
|
4
|
-
"version": "0.1.
|
|
3
|
+
"description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite store, Markdown mirrors, 6 model tools, automatic injection, session summarization, and a Web GUI panel",
|
|
4
|
+
"version": "0.1.3",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
package/src/dream.js
CHANGED
|
@@ -62,6 +62,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
62
62
|
let running = false;
|
|
63
63
|
let disposed = false;
|
|
64
64
|
let baseline = { count: 0, chars: 0 };
|
|
65
|
+
let inFlight = null;
|
|
65
66
|
|
|
66
67
|
function shouldTrigger(service) {
|
|
67
68
|
const memories = service.all().filter((m) => !m.archived && m.type !== "summary");
|
|
@@ -81,8 +82,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
81
82
|
running = true;
|
|
82
83
|
// Defer the onRun invocation so a synchronous throw cannot escape the
|
|
83
84
|
// timer callback (which would crash the process) and skip the teardown.
|
|
84
|
-
// Errors are logged, never swallowed silently.
|
|
85
|
-
|
|
85
|
+
// Errors are logged, never swallowed silently. inFlight lets dispose()
|
|
86
|
+
// await the running consolidation before the caller closes the store.
|
|
87
|
+
inFlight = Promise.resolve()
|
|
86
88
|
.then(() => (onRun ? onRun() : Promise.resolve({ ok: true, skipped: true })))
|
|
87
89
|
.then((result) => {
|
|
88
90
|
// Refresh the baseline only for a successful run (design §5.3: an
|
|
@@ -105,17 +107,19 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
105
107
|
})
|
|
106
108
|
.finally(() => {
|
|
107
109
|
running = false;
|
|
110
|
+
inFlight = null;
|
|
108
111
|
});
|
|
109
112
|
}, delayMs);
|
|
110
113
|
return true;
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
function dispose() {
|
|
116
|
+
async function dispose() {
|
|
114
117
|
disposed = true;
|
|
115
118
|
if (pendingTimer) { clearTimeout(pendingTimer); pendingTimer = null; }
|
|
116
|
-
// An in-flight run is left to complete naturally
|
|
117
|
-
// already paid for and aborting would discard the work.
|
|
118
|
-
//
|
|
119
|
+
// An in-flight run is left to complete naturally (its LLM calls are
|
|
120
|
+
// already paid for and aborting would discard the work). Await it so the
|
|
121
|
+
// caller can close the store only after every write has landed.
|
|
122
|
+
if (inFlight) await inFlight.catch(() => {});
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
async function runDream(ctx, service, config) {
|
package/src/index.js
CHANGED
|
@@ -34,9 +34,15 @@ export const apply = (ctx, config) => {
|
|
|
34
34
|
const service = createService({ store, mirror, config: cfg });
|
|
35
35
|
|
|
36
36
|
// Human edits in mirror files win on every sync; merge them back first.
|
|
37
|
-
// TYPE_FILE maps each memory type to its mirror filename.
|
|
37
|
+
// TYPE_FILE maps each memory type to its mirror filename. Read every type's
|
|
38
|
+
// edits up front: mergeHumanEdits re-renders ALL mirror files on success, so
|
|
39
|
+
// a per-type read-then-merge loop would overwrite edits in files not yet read
|
|
40
|
+
// (e.g. preferences.md merging would clobber unsynced projects.md edits).
|
|
41
|
+
const humanEdits = new Map();
|
|
38
42
|
for (const type of Object.keys(TYPE_FILE)) {
|
|
39
|
-
|
|
43
|
+
humanEdits.set(type, mirror.readHumanEdits(type));
|
|
44
|
+
}
|
|
45
|
+
for (const [type, edits] of humanEdits) {
|
|
40
46
|
if (edits.length) service.mergeHumanEdits(type, edits);
|
|
41
47
|
}
|
|
42
48
|
|
|
@@ -76,11 +82,14 @@ export const apply = (ctx, config) => {
|
|
|
76
82
|
disposers.push(api.dispose);
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
// Async disposer: cordis awaits the returned promise on unload (runDisposable),
|
|
86
|
+
// so an in-flight dream run is allowed to finish before the SQLite store is
|
|
87
|
+
// closed — dream.dispose() resolves only after its current run settles.
|
|
88
|
+
return async () => {
|
|
80
89
|
for (const dispose of disposers) {
|
|
81
90
|
if (typeof dispose === "function") dispose();
|
|
82
91
|
}
|
|
83
|
-
if (dream) dream.dispose();
|
|
92
|
+
if (dream) await dream.dispose();
|
|
84
93
|
store.close();
|
|
85
94
|
};
|
|
86
95
|
};
|
package/src/store.js
CHANGED
|
@@ -193,8 +193,9 @@ export function createStore(path) {
|
|
|
193
193
|
function search(query, { limit = 20, includeArchived = false } = {}) {
|
|
194
194
|
const q = String(query).trim();
|
|
195
195
|
if (!q) return [];
|
|
196
|
-
//
|
|
197
|
-
//
|
|
196
|
+
// Plain LIKE substring scan over title/content/tags (wildcards escaped so
|
|
197
|
+
// user input matches literally). No FTS5: CJK substring matching needs
|
|
198
|
+
// LIKE, and typical memory stores are small enough that a scan is fine.
|
|
198
199
|
const like = `%${escapeLike(q)}%`;
|
|
199
200
|
const { limit: lim } = sanitizePage(limit, 0, 20);
|
|
200
201
|
const archivedFilter = includeArchived ? "" : "archived = 0 AND ";
|