@modusensus/dsh-mneme 0.1.0 → 0.1.2
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 +178 -179
- package/cordis.patch.yml +15 -0
- package/lib/api.js +59 -59
- package/lib/client.js +187 -187
- package/lib/config.js +15 -15
- package/lib/dream/decisions.js +121 -121
- package/lib/dream.js +205 -205
- package/lib/index.js +86 -86
- package/lib/inject.js +22 -22
- package/lib/mirror.js +131 -131
- package/lib/service.js +157 -157
- package/lib/store.js +230 -230
- package/lib/summarize.js +171 -171
- package/lib/tools.js +221 -221
- package/package.json +58 -32
- package/src/api.js +59 -59
- package/src/config.js +15 -15
- package/src/dream/decisions.js +121 -121
- package/src/dream.js +205 -205
- package/src/index.js +86 -86
- package/src/inject.js +22 -22
- package/src/mirror.js +131 -131
- package/src/service.js +157 -157
- package/src/store.js +230 -230
- package/src/summarize.js +171 -171
- package/src/tools.js +221 -221
package/lib/client.js
CHANGED
|
@@ -1,187 +1,187 @@
|
|
|
1
|
-
window.__ModuleLoader__.load({
|
|
2
|
-
id: "dsh-mneme",
|
|
3
|
-
factory: (require) => {
|
|
4
|
-
var module = { exports: {} };
|
|
5
|
-
var exports = module.exports;
|
|
6
|
-
|
|
7
|
-
let react = require("react");
|
|
8
|
-
let reactDom = require("react-dom");
|
|
9
|
-
let { useState, useEffect, useCallback, useRef } = react;
|
|
10
|
-
let { createPortal } = reactDom;
|
|
11
|
-
|
|
12
|
-
const inject = ["slots", "locale"];
|
|
13
|
-
|
|
14
|
-
const NS = "memory";
|
|
15
|
-
|
|
16
|
-
const dictionaries = {
|
|
17
|
-
zh: {
|
|
18
|
-
"memory.panel.title": "记忆库",
|
|
19
|
-
"memory.panel.search": "搜索记忆…",
|
|
20
|
-
"memory.panel.empty": "暂无记忆条目",
|
|
21
|
-
"memory.panel.open": "记忆",
|
|
22
|
-
"memory.tab.all": "全部",
|
|
23
|
-
"memory.tab.preference": "偏好",
|
|
24
|
-
"memory.tab.project": "项目",
|
|
25
|
-
"memory.tab.decision": "决策",
|
|
26
|
-
"memory.tab.history": "历史"
|
|
27
|
-
},
|
|
28
|
-
en: {
|
|
29
|
-
"memory.panel.title": "Memory",
|
|
30
|
-
"memory.panel.search": "Search memories…",
|
|
31
|
-
"memory.panel.empty": "No memories yet",
|
|
32
|
-
"memory.panel.open": "Memory",
|
|
33
|
-
"memory.tab.all": "All",
|
|
34
|
-
"memory.tab.preference": "Preferences",
|
|
35
|
-
"memory.tab.project": "Projects",
|
|
36
|
-
"memory.tab.decision": "Decisions",
|
|
37
|
-
"memory.tab.history": "History"
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
function typeLabel(t, type) {
|
|
42
|
-
const key = `memory.tab.${type}`;
|
|
43
|
-
const label = t(key);
|
|
44
|
-
return label && label !== key ? label : String(type);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function formatDate(value) {
|
|
48
|
-
if (!value) return "—";
|
|
49
|
-
const date = new Date(value);
|
|
50
|
-
return Number.isNaN(date.getTime()) ? "—" : date.toLocaleString();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function MemoryPanel({ t, onClose }) {
|
|
54
|
-
const [tab, setTab] = useState("all");
|
|
55
|
-
const [query, setQuery] = useState("");
|
|
56
|
-
const [items, setItems] = useState([]);
|
|
57
|
-
const [loading, setLoading] = useState(false);
|
|
58
|
-
const abortRef = useRef(null);
|
|
59
|
-
|
|
60
|
-
const load = useCallback(async () => {
|
|
61
|
-
abortRef.current?.abort();
|
|
62
|
-
const controller = new AbortController();
|
|
63
|
-
abortRef.current = controller;
|
|
64
|
-
setLoading(true);
|
|
65
|
-
try {
|
|
66
|
-
const params = new URLSearchParams();
|
|
67
|
-
if (tab !== "all") params.set("type", tab);
|
|
68
|
-
const url = query.trim()
|
|
69
|
-
? `/api/dsh-mneme/search?q=${encodeURIComponent(query.trim())}`
|
|
70
|
-
: `/api/dsh-mneme/list?${params.toString()}`;
|
|
71
|
-
const res = await fetch(url, { signal: controller.signal });
|
|
72
|
-
const data = await res.json();
|
|
73
|
-
setItems(data.items || []);
|
|
74
|
-
} catch (error) {
|
|
75
|
-
if (error.name === "AbortError") return;
|
|
76
|
-
setItems([]);
|
|
77
|
-
} finally {
|
|
78
|
-
setLoading(false);
|
|
79
|
-
}
|
|
80
|
-
}, [tab, query]);
|
|
81
|
-
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
load();
|
|
84
|
-
return () => abortRef.current?.abort();
|
|
85
|
-
}, [load]);
|
|
86
|
-
|
|
87
|
-
const tabs = ["all", "preference", "project", "decision", "history"];
|
|
88
|
-
|
|
89
|
-
return createPortal(
|
|
90
|
-
react.createElement("div", { style: styles.overlay },
|
|
91
|
-
react.createElement("div", { style: styles.panel },
|
|
92
|
-
react.createElement("div", { style: styles.header },
|
|
93
|
-
react.createElement("span", { style: styles.title }, t("memory.panel.title")),
|
|
94
|
-
react.createElement("button", { style: styles.close, onClick: onClose }, "×")
|
|
95
|
-
),
|
|
96
|
-
react.createElement("input", {
|
|
97
|
-
style: styles.search,
|
|
98
|
-
placeholder: t("memory.panel.search"),
|
|
99
|
-
value: query,
|
|
100
|
-
onChange: (e) => setQuery(e.target.value)
|
|
101
|
-
}),
|
|
102
|
-
react.createElement("div", { style: styles.tabs },
|
|
103
|
-
tabs.map((key) =>
|
|
104
|
-
react.createElement("button", {
|
|
105
|
-
key,
|
|
106
|
-
style: { ...styles.tab, ...(tab === key ? styles.tabActive : {}) },
|
|
107
|
-
onClick: () => setTab(key)
|
|
108
|
-
}, t(`memory.tab.${key}`))
|
|
109
|
-
)
|
|
110
|
-
),
|
|
111
|
-
react.createElement("div", { style: styles.list },
|
|
112
|
-
loading
|
|
113
|
-
? react.createElement("div", { style: styles.hint }, "…")
|
|
114
|
-
: items.length === 0
|
|
115
|
-
? react.createElement("div", { style: styles.hint }, t("memory.panel.empty"))
|
|
116
|
-
: items.map((item) =>
|
|
117
|
-
react.createElement("div", { key: item.id, style: styles.card },
|
|
118
|
-
react.createElement("div", { style: styles.cardTitle },
|
|
119
|
-
react.createElement("span", null, item.title),
|
|
120
|
-
react.createElement("span", { style: styles.badge },
|
|
121
|
-
`${typeLabel(t, item.type)} · ★${item.importance}`
|
|
122
|
-
)
|
|
123
|
-
),
|
|
124
|
-
react.createElement("div", { style: styles.cardContent }, item.content),
|
|
125
|
-
react.createElement("div", { style: styles.cardMeta },
|
|
126
|
-
formatDate(item.updated_at)
|
|
127
|
-
)
|
|
128
|
-
)
|
|
129
|
-
)
|
|
130
|
-
)
|
|
131
|
-
)
|
|
132
|
-
),
|
|
133
|
-
document.body
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const styles = {
|
|
138
|
-
overlay: { position: "fixed", inset: 0, background: "rgba(0,0,0,0.4)", zIndex: 1000, display: "flex", alignItems: "center", justifyContent: "center" },
|
|
139
|
-
panel: { background: "var(--dsw-alias-bg-base, #fff)", borderRadius: 12, width: 640, maxWidth: "90vw", maxHeight: "80vh", display: "flex", flexDirection: "column", padding: 16, boxShadow: "0 8px 40px rgba(0,0,0,0.2)" },
|
|
140
|
-
header: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 },
|
|
141
|
-
title: { fontSize: 16, fontWeight: 600 },
|
|
142
|
-
close: { border: "none", background: "none", fontSize: 20, cursor: "pointer", color: "var(--dsw-alias-label-secondary, #666)" },
|
|
143
|
-
search: { padding: "8px 12px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2, #ddd)", marginBottom: 12, fontSize: 14 },
|
|
144
|
-
tabs: { display: "flex", gap: 6, marginBottom: 12, flexWrap: "wrap" },
|
|
145
|
-
tab: { padding: "4px 10px", borderRadius: 999, border: "1px solid var(--dsw-alias-border-l2, #ddd)", background: "none", cursor: "pointer", fontSize: 12 },
|
|
146
|
-
tabActive: { background: "var(--dsw-alias-interactive-bg-active, #eee)" },
|
|
147
|
-
list: { overflowY: "auto", display: "flex", flexDirection: "column", gap: 8 },
|
|
148
|
-
hint: { color: "var(--dsw-alias-label-tertiary, #999)", padding: "24px 0", textAlign: "center" },
|
|
149
|
-
card: { border: "1px solid var(--dsw-alias-border-l1, #eee)", borderRadius: 8, padding: "10px 12px" },
|
|
150
|
-
cardTitle: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 4, fontSize: 14, fontWeight: 600 },
|
|
151
|
-
badge: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)" },
|
|
152
|
-
cardContent: { fontSize: 13, color: "var(--dsw-alias-label-secondary, #555)", marginBottom: 4, whiteSpace: "pre-wrap", wordBreak: "break-word" },
|
|
153
|
-
cardMeta: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)" },
|
|
154
|
-
footerButton: { padding: "4px 10px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2, #ddd)", background: "none", cursor: "pointer", fontSize: 12, margin: "2px 8px" },
|
|
155
|
-
footerButtonActive: { background: "var(--dsw-alias-interactive-bg-active, #eee)" }
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
function apply(ctx) {
|
|
159
|
-
ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-mneme: dictionaries");
|
|
160
|
-
|
|
161
|
-
ctx.effect(() => {
|
|
162
|
-
const t = ctx.locale.bind(NS);
|
|
163
|
-
return ctx.slots.inject("sidebar.footer.action", () =>
|
|
164
|
-
ctx.slots.register({
|
|
165
|
-
name: "sidebar.footer.action",
|
|
166
|
-
id: "memory",
|
|
167
|
-
locale: NS,
|
|
168
|
-
inject: () => ({})
|
|
169
|
-
}, () => {
|
|
170
|
-
const [open, setOpen] = react.useState(false);
|
|
171
|
-
return react.createElement(react.Fragment, null,
|
|
172
|
-
react.createElement("button", {
|
|
173
|
-
onClick: () => setOpen(true),
|
|
174
|
-
style: { ...styles.footerButton, ...(open ? styles.footerButtonActive : {}) }
|
|
175
|
-
}, t("memory.panel.open")),
|
|
176
|
-
open && react.createElement(MemoryPanel, { t, onClose: () => setOpen(false) })
|
|
177
|
-
);
|
|
178
|
-
})
|
|
179
|
-
);
|
|
180
|
-
}, "dsh-mneme: sidebar action");
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
exports.apply = apply;
|
|
184
|
-
exports.inject = inject;
|
|
185
|
-
return module.exports;
|
|
186
|
-
}
|
|
187
|
-
});
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@modusensus/dsh-mneme",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let reactDom = require("react-dom");
|
|
9
|
+
let { useState, useEffect, useCallback, useRef } = react;
|
|
10
|
+
let { createPortal } = reactDom;
|
|
11
|
+
|
|
12
|
+
const inject = ["slots", "locale"];
|
|
13
|
+
|
|
14
|
+
const NS = "memory";
|
|
15
|
+
|
|
16
|
+
const dictionaries = {
|
|
17
|
+
zh: {
|
|
18
|
+
"memory.panel.title": "记忆库",
|
|
19
|
+
"memory.panel.search": "搜索记忆…",
|
|
20
|
+
"memory.panel.empty": "暂无记忆条目",
|
|
21
|
+
"memory.panel.open": "记忆",
|
|
22
|
+
"memory.tab.all": "全部",
|
|
23
|
+
"memory.tab.preference": "偏好",
|
|
24
|
+
"memory.tab.project": "项目",
|
|
25
|
+
"memory.tab.decision": "决策",
|
|
26
|
+
"memory.tab.history": "历史"
|
|
27
|
+
},
|
|
28
|
+
en: {
|
|
29
|
+
"memory.panel.title": "Memory",
|
|
30
|
+
"memory.panel.search": "Search memories…",
|
|
31
|
+
"memory.panel.empty": "No memories yet",
|
|
32
|
+
"memory.panel.open": "Memory",
|
|
33
|
+
"memory.tab.all": "All",
|
|
34
|
+
"memory.tab.preference": "Preferences",
|
|
35
|
+
"memory.tab.project": "Projects",
|
|
36
|
+
"memory.tab.decision": "Decisions",
|
|
37
|
+
"memory.tab.history": "History"
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function typeLabel(t, type) {
|
|
42
|
+
const key = `memory.tab.${type}`;
|
|
43
|
+
const label = t(key);
|
|
44
|
+
return label && label !== key ? label : String(type);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatDate(value) {
|
|
48
|
+
if (!value) return "—";
|
|
49
|
+
const date = new Date(value);
|
|
50
|
+
return Number.isNaN(date.getTime()) ? "—" : date.toLocaleString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function MemoryPanel({ t, onClose }) {
|
|
54
|
+
const [tab, setTab] = useState("all");
|
|
55
|
+
const [query, setQuery] = useState("");
|
|
56
|
+
const [items, setItems] = useState([]);
|
|
57
|
+
const [loading, setLoading] = useState(false);
|
|
58
|
+
const abortRef = useRef(null);
|
|
59
|
+
|
|
60
|
+
const load = useCallback(async () => {
|
|
61
|
+
abortRef.current?.abort();
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
abortRef.current = controller;
|
|
64
|
+
setLoading(true);
|
|
65
|
+
try {
|
|
66
|
+
const params = new URLSearchParams();
|
|
67
|
+
if (tab !== "all") params.set("type", tab);
|
|
68
|
+
const url = query.trim()
|
|
69
|
+
? `/api/dsh-mneme/search?q=${encodeURIComponent(query.trim())}`
|
|
70
|
+
: `/api/dsh-mneme/list?${params.toString()}`;
|
|
71
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
72
|
+
const data = await res.json();
|
|
73
|
+
setItems(data.items || []);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error.name === "AbortError") return;
|
|
76
|
+
setItems([]);
|
|
77
|
+
} finally {
|
|
78
|
+
setLoading(false);
|
|
79
|
+
}
|
|
80
|
+
}, [tab, query]);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
load();
|
|
84
|
+
return () => abortRef.current?.abort();
|
|
85
|
+
}, [load]);
|
|
86
|
+
|
|
87
|
+
const tabs = ["all", "preference", "project", "decision", "history"];
|
|
88
|
+
|
|
89
|
+
return createPortal(
|
|
90
|
+
react.createElement("div", { style: styles.overlay },
|
|
91
|
+
react.createElement("div", { style: styles.panel },
|
|
92
|
+
react.createElement("div", { style: styles.header },
|
|
93
|
+
react.createElement("span", { style: styles.title }, t("memory.panel.title")),
|
|
94
|
+
react.createElement("button", { style: styles.close, onClick: onClose }, "×")
|
|
95
|
+
),
|
|
96
|
+
react.createElement("input", {
|
|
97
|
+
style: styles.search,
|
|
98
|
+
placeholder: t("memory.panel.search"),
|
|
99
|
+
value: query,
|
|
100
|
+
onChange: (e) => setQuery(e.target.value)
|
|
101
|
+
}),
|
|
102
|
+
react.createElement("div", { style: styles.tabs },
|
|
103
|
+
tabs.map((key) =>
|
|
104
|
+
react.createElement("button", {
|
|
105
|
+
key,
|
|
106
|
+
style: { ...styles.tab, ...(tab === key ? styles.tabActive : {}) },
|
|
107
|
+
onClick: () => setTab(key)
|
|
108
|
+
}, t(`memory.tab.${key}`))
|
|
109
|
+
)
|
|
110
|
+
),
|
|
111
|
+
react.createElement("div", { style: styles.list },
|
|
112
|
+
loading
|
|
113
|
+
? react.createElement("div", { style: styles.hint }, "…")
|
|
114
|
+
: items.length === 0
|
|
115
|
+
? react.createElement("div", { style: styles.hint }, t("memory.panel.empty"))
|
|
116
|
+
: items.map((item) =>
|
|
117
|
+
react.createElement("div", { key: item.id, style: styles.card },
|
|
118
|
+
react.createElement("div", { style: styles.cardTitle },
|
|
119
|
+
react.createElement("span", null, item.title),
|
|
120
|
+
react.createElement("span", { style: styles.badge },
|
|
121
|
+
`${typeLabel(t, item.type)} · ★${item.importance}`
|
|
122
|
+
)
|
|
123
|
+
),
|
|
124
|
+
react.createElement("div", { style: styles.cardContent }, item.content),
|
|
125
|
+
react.createElement("div", { style: styles.cardMeta },
|
|
126
|
+
formatDate(item.updated_at)
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
),
|
|
133
|
+
document.body
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const styles = {
|
|
138
|
+
overlay: { position: "fixed", inset: 0, background: "rgba(0,0,0,0.4)", zIndex: 1000, display: "flex", alignItems: "center", justifyContent: "center" },
|
|
139
|
+
panel: { background: "var(--dsw-alias-bg-base, #fff)", borderRadius: 12, width: 640, maxWidth: "90vw", maxHeight: "80vh", display: "flex", flexDirection: "column", padding: 16, boxShadow: "0 8px 40px rgba(0,0,0,0.2)" },
|
|
140
|
+
header: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 },
|
|
141
|
+
title: { fontSize: 16, fontWeight: 600 },
|
|
142
|
+
close: { border: "none", background: "none", fontSize: 20, cursor: "pointer", color: "var(--dsw-alias-label-secondary, #666)" },
|
|
143
|
+
search: { padding: "8px 12px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2, #ddd)", marginBottom: 12, fontSize: 14 },
|
|
144
|
+
tabs: { display: "flex", gap: 6, marginBottom: 12, flexWrap: "wrap" },
|
|
145
|
+
tab: { padding: "4px 10px", borderRadius: 999, border: "1px solid var(--dsw-alias-border-l2, #ddd)", background: "none", cursor: "pointer", fontSize: 12 },
|
|
146
|
+
tabActive: { background: "var(--dsw-alias-interactive-bg-active, #eee)" },
|
|
147
|
+
list: { overflowY: "auto", display: "flex", flexDirection: "column", gap: 8 },
|
|
148
|
+
hint: { color: "var(--dsw-alias-label-tertiary, #999)", padding: "24px 0", textAlign: "center" },
|
|
149
|
+
card: { border: "1px solid var(--dsw-alias-border-l1, #eee)", borderRadius: 8, padding: "10px 12px" },
|
|
150
|
+
cardTitle: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 4, fontSize: 14, fontWeight: 600 },
|
|
151
|
+
badge: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)" },
|
|
152
|
+
cardContent: { fontSize: 13, color: "var(--dsw-alias-label-secondary, #555)", marginBottom: 4, whiteSpace: "pre-wrap", wordBreak: "break-word" },
|
|
153
|
+
cardMeta: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)" },
|
|
154
|
+
footerButton: { padding: "4px 10px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2, #ddd)", background: "none", cursor: "pointer", fontSize: 12, margin: "2px 8px" },
|
|
155
|
+
footerButtonActive: { background: "var(--dsw-alias-interactive-bg-active, #eee)" }
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
function apply(ctx) {
|
|
159
|
+
ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-mneme: dictionaries");
|
|
160
|
+
|
|
161
|
+
ctx.effect(() => {
|
|
162
|
+
const t = ctx.locale.bind(NS);
|
|
163
|
+
return ctx.slots.inject("sidebar.footer.action", () =>
|
|
164
|
+
ctx.slots.register({
|
|
165
|
+
name: "sidebar.footer.action",
|
|
166
|
+
id: "memory",
|
|
167
|
+
locale: NS,
|
|
168
|
+
inject: () => ({})
|
|
169
|
+
}, () => {
|
|
170
|
+
const [open, setOpen] = react.useState(false);
|
|
171
|
+
return react.createElement(react.Fragment, null,
|
|
172
|
+
react.createElement("button", {
|
|
173
|
+
onClick: () => setOpen(true),
|
|
174
|
+
style: { ...styles.footerButton, ...(open ? styles.footerButtonActive : {}) }
|
|
175
|
+
}, t("memory.panel.open")),
|
|
176
|
+
open && react.createElement(MemoryPanel, { t, onClose: () => setOpen(false) })
|
|
177
|
+
);
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
}, "dsh-mneme: sidebar action");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
exports.apply = apply;
|
|
184
|
+
exports.inject = inject;
|
|
185
|
+
return module.exports;
|
|
186
|
+
}
|
|
187
|
+
});
|
package/lib/config.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
|
|
3
|
-
export const Config = z.object({
|
|
4
|
-
memoryDir: z.string().default("~/.dsh/memory"),
|
|
5
|
-
autoInject: z.boolean().default(true),
|
|
6
|
-
autoSummarize: z.boolean().default(true),
|
|
7
|
-
maxInjectedItems: z.natural().min(1).max(20).default(5),
|
|
8
|
-
importanceThreshold: z.natural().min(1).max(5).default(3),
|
|
9
|
-
autoDream: z.boolean().default(true),
|
|
10
|
-
dreamThresholdCount: z.natural().min(1).max(1000).default(10),
|
|
11
|
-
dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
|
|
12
|
-
dreamDelayMs: z.natural().min(0).max(60000).default(2000),
|
|
13
|
-
dreamProvider: z.string(),
|
|
14
|
-
dreamModel: z.string()
|
|
15
|
-
});
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
|
|
3
|
+
export const Config = z.object({
|
|
4
|
+
memoryDir: z.string().default("~/.dsh/memory"),
|
|
5
|
+
autoInject: z.boolean().default(true),
|
|
6
|
+
autoSummarize: z.boolean().default(true),
|
|
7
|
+
maxInjectedItems: z.natural().min(1).max(20).default(5),
|
|
8
|
+
importanceThreshold: z.natural().min(1).max(5).default(3),
|
|
9
|
+
autoDream: z.boolean().default(true),
|
|
10
|
+
dreamThresholdCount: z.natural().min(1).max(1000).default(10),
|
|
11
|
+
dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
|
|
12
|
+
dreamDelayMs: z.natural().min(0).max(60000).default(2000),
|
|
13
|
+
dreamProvider: z.string(),
|
|
14
|
+
dreamModel: z.string()
|
|
15
|
+
});
|
package/lib/dream/decisions.js
CHANGED
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
const ACTIONS = new Set(["keep", "merge", "archive", "conflict"]);
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Validate a dream decision list against a snapshot of eligible memories.
|
|
5
|
-
* @param decisions - LLM-produced decision list.
|
|
6
|
-
* @param snapshot - Map<id, memory> of eligible (non-archived, non-summary) entries.
|
|
7
|
-
* @returns {{ok: boolean, errors: string[]}}
|
|
8
|
-
*/
|
|
9
|
-
export function validateDecisions(decisions, snapshot) {
|
|
10
|
-
const errors = [];
|
|
11
|
-
if (!Array.isArray(decisions) || decisions.length === 0) {
|
|
12
|
-
return { ok: false, errors: ["decision list must be a non-empty array"] };
|
|
13
|
-
}
|
|
14
|
-
const claimed = new Set();
|
|
15
|
-
for (const [index, d] of decisions.entries()) {
|
|
16
|
-
const at = `decision[${index}]`;
|
|
17
|
-
if (!d || typeof d !== "object" || !ACTIONS.has(d.action)) {
|
|
18
|
-
errors.push(`${at}: invalid action ${JSON.stringify(d?.action)}`);
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
const ids = d.action === "conflict" ? [d.winner, d.loser] : (d.ids ?? []);
|
|
22
|
-
if (d.action === "conflict") {
|
|
23
|
-
if (!d.winner || !d.loser || d.winner === d.loser) {
|
|
24
|
-
errors.push(`${at}: conflict needs distinct winner and loser`);
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
} else if (!Array.isArray(d.ids) || d.ids.length === 0) {
|
|
28
|
-
errors.push(`${at}: ${d.action} needs non-empty ids`);
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
for (const id of ids) {
|
|
32
|
-
const mem = snapshot.get(id);
|
|
33
|
-
if (!mem) {
|
|
34
|
-
errors.push(`${at}: unknown id ${JSON.stringify(id)}`);
|
|
35
|
-
} else if (mem.archived || mem.type === "summary") {
|
|
36
|
-
errors.push(`${at}: id ${JSON.stringify(id)} is archived or summary (not eligible)`);
|
|
37
|
-
}
|
|
38
|
-
if (claimed.has(id)) {
|
|
39
|
-
errors.push(`${at}: id ${JSON.stringify(id)} claimed by multiple decisions`);
|
|
40
|
-
}
|
|
41
|
-
claimed.add(id);
|
|
42
|
-
}
|
|
43
|
-
if (d.action === "merge") {
|
|
44
|
-
if (!d.keepSource || !d.ids.includes(d.keepSource)) {
|
|
45
|
-
errors.push(`${at}: merge keepSource must be one of ids`);
|
|
46
|
-
}
|
|
47
|
-
if (typeof d.title !== "string" || !d.title.trim() || typeof d.content !== "string" || !d.content.trim()) {
|
|
48
|
-
errors.push(`${at}: merge needs non-empty title and content`);
|
|
49
|
-
}
|
|
50
|
-
if (d.importance !== undefined && (!Number.isInteger(d.importance) || d.importance < 1 || d.importance > 5)) {
|
|
51
|
-
errors.push(`${at}: merge importance must be an integer 1-5 when provided`);
|
|
52
|
-
}
|
|
53
|
-
// Merging across types would blur preference/project/decision boundaries
|
|
54
|
-
// in the injected context; the snapshot carries each entry's type.
|
|
55
|
-
const mergeTypes = new Set(d.ids.map((id) => snapshot.get(id)?.type));
|
|
56
|
-
if (mergeTypes.size > 1) {
|
|
57
|
-
errors.push(`${at}: merge ids span multiple types (${[...mergeTypes].join(", ")})`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// Every snapshot id must appear in at least one decision
|
|
62
|
-
for (const id of snapshot.keys()) {
|
|
63
|
-
if (!claimed.has(id)) errors.push(`memory ${JSON.stringify(id)} missing from decisions`);
|
|
64
|
-
}
|
|
65
|
-
return { ok: errors.length === 0, errors };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Apply a validated decision list to the service. Caller must validate first.
|
|
70
|
-
*
|
|
71
|
-
* Note: merge is intentionally non-atomic — the keeper is updated before the
|
|
72
|
-
* other sources are archived, so a failure between the two never loses content.
|
|
73
|
-
*
|
|
74
|
-
* @param decisions - validated decision list.
|
|
75
|
-
* @param service - memory service (saveWithDedupe/getById/update/setArchived).
|
|
76
|
-
* @param logger - optional logger ({ warn }); per-decision failures are logged.
|
|
77
|
-
* @returns number of applied decisions (archive counts each archived memory as one).
|
|
78
|
-
*/
|
|
79
|
-
export function applyDecisions(decisions, service, logger = null) {
|
|
80
|
-
let applied = 0;
|
|
81
|
-
for (const [i, d] of decisions.entries()) {
|
|
82
|
-
try {
|
|
83
|
-
if (d.action === "keep") continue;
|
|
84
|
-
if (d.action === "archive") {
|
|
85
|
-
for (const id of d.ids) {
|
|
86
|
-
const mem = service.getById(id);
|
|
87
|
-
if (mem && !mem.archived) { service.setArchived(id, true); applied++; }
|
|
88
|
-
}
|
|
89
|
-
} else if (d.action === "merge") {
|
|
90
|
-
const keeper = service.getById(d.keepSource);
|
|
91
|
-
if (!keeper || keeper.archived) continue;
|
|
92
|
-
service.update(d.keepSource, {
|
|
93
|
-
title: d.title,
|
|
94
|
-
content: d.content,
|
|
95
|
-
importance: d.importance ?? Math.max(keeper.importance, ...d.ids.map((id) => service.getById(id)?.importance ?? 1))
|
|
96
|
-
});
|
|
97
|
-
for (const id of d.ids) {
|
|
98
|
-
if (id !== d.keepSource) {
|
|
99
|
-
const mem = service.getById(id);
|
|
100
|
-
if (mem && !mem.archived) { service.setArchived(id, true); }
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
applied++;
|
|
104
|
-
} else if (d.action === "conflict") {
|
|
105
|
-
const winner = service.getById(d.winner);
|
|
106
|
-
const loser = service.getById(d.loser);
|
|
107
|
-
if (!winner || !loser) continue;
|
|
108
|
-
service.update(d.winner, {
|
|
109
|
-
content: `${winner.content}\n\n(已否决旧信息:${[...loser.content].slice(0, 100).join("")})`
|
|
110
|
-
});
|
|
111
|
-
service.setArchived(d.loser, true);
|
|
112
|
-
applied++;
|
|
113
|
-
}
|
|
114
|
-
} catch (error) {
|
|
115
|
-
// Skip individual bad decision; never corrupt the store. The optional
|
|
116
|
-
// logger makes the failure visible instead of failing silently.
|
|
117
|
-
logger?.warn?.(`dsh-mneme dream: failed to apply ${d.action} at index ${i}: ${error.message}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return applied;
|
|
121
|
-
}
|
|
1
|
+
const ACTIONS = new Set(["keep", "merge", "archive", "conflict"]);
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validate a dream decision list against a snapshot of eligible memories.
|
|
5
|
+
* @param decisions - LLM-produced decision list.
|
|
6
|
+
* @param snapshot - Map<id, memory> of eligible (non-archived, non-summary) entries.
|
|
7
|
+
* @returns {{ok: boolean, errors: string[]}}
|
|
8
|
+
*/
|
|
9
|
+
export function validateDecisions(decisions, snapshot) {
|
|
10
|
+
const errors = [];
|
|
11
|
+
if (!Array.isArray(decisions) || decisions.length === 0) {
|
|
12
|
+
return { ok: false, errors: ["decision list must be a non-empty array"] };
|
|
13
|
+
}
|
|
14
|
+
const claimed = new Set();
|
|
15
|
+
for (const [index, d] of decisions.entries()) {
|
|
16
|
+
const at = `decision[${index}]`;
|
|
17
|
+
if (!d || typeof d !== "object" || !ACTIONS.has(d.action)) {
|
|
18
|
+
errors.push(`${at}: invalid action ${JSON.stringify(d?.action)}`);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const ids = d.action === "conflict" ? [d.winner, d.loser] : (d.ids ?? []);
|
|
22
|
+
if (d.action === "conflict") {
|
|
23
|
+
if (!d.winner || !d.loser || d.winner === d.loser) {
|
|
24
|
+
errors.push(`${at}: conflict needs distinct winner and loser`);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
} else if (!Array.isArray(d.ids) || d.ids.length === 0) {
|
|
28
|
+
errors.push(`${at}: ${d.action} needs non-empty ids`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
for (const id of ids) {
|
|
32
|
+
const mem = snapshot.get(id);
|
|
33
|
+
if (!mem) {
|
|
34
|
+
errors.push(`${at}: unknown id ${JSON.stringify(id)}`);
|
|
35
|
+
} else if (mem.archived || mem.type === "summary") {
|
|
36
|
+
errors.push(`${at}: id ${JSON.stringify(id)} is archived or summary (not eligible)`);
|
|
37
|
+
}
|
|
38
|
+
if (claimed.has(id)) {
|
|
39
|
+
errors.push(`${at}: id ${JSON.stringify(id)} claimed by multiple decisions`);
|
|
40
|
+
}
|
|
41
|
+
claimed.add(id);
|
|
42
|
+
}
|
|
43
|
+
if (d.action === "merge") {
|
|
44
|
+
if (!d.keepSource || !d.ids.includes(d.keepSource)) {
|
|
45
|
+
errors.push(`${at}: merge keepSource must be one of ids`);
|
|
46
|
+
}
|
|
47
|
+
if (typeof d.title !== "string" || !d.title.trim() || typeof d.content !== "string" || !d.content.trim()) {
|
|
48
|
+
errors.push(`${at}: merge needs non-empty title and content`);
|
|
49
|
+
}
|
|
50
|
+
if (d.importance !== undefined && (!Number.isInteger(d.importance) || d.importance < 1 || d.importance > 5)) {
|
|
51
|
+
errors.push(`${at}: merge importance must be an integer 1-5 when provided`);
|
|
52
|
+
}
|
|
53
|
+
// Merging across types would blur preference/project/decision boundaries
|
|
54
|
+
// in the injected context; the snapshot carries each entry's type.
|
|
55
|
+
const mergeTypes = new Set(d.ids.map((id) => snapshot.get(id)?.type));
|
|
56
|
+
if (mergeTypes.size > 1) {
|
|
57
|
+
errors.push(`${at}: merge ids span multiple types (${[...mergeTypes].join(", ")})`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Every snapshot id must appear in at least one decision
|
|
62
|
+
for (const id of snapshot.keys()) {
|
|
63
|
+
if (!claimed.has(id)) errors.push(`memory ${JSON.stringify(id)} missing from decisions`);
|
|
64
|
+
}
|
|
65
|
+
return { ok: errors.length === 0, errors };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Apply a validated decision list to the service. Caller must validate first.
|
|
70
|
+
*
|
|
71
|
+
* Note: merge is intentionally non-atomic — the keeper is updated before the
|
|
72
|
+
* other sources are archived, so a failure between the two never loses content.
|
|
73
|
+
*
|
|
74
|
+
* @param decisions - validated decision list.
|
|
75
|
+
* @param service - memory service (saveWithDedupe/getById/update/setArchived).
|
|
76
|
+
* @param logger - optional logger ({ warn }); per-decision failures are logged.
|
|
77
|
+
* @returns number of applied decisions (archive counts each archived memory as one).
|
|
78
|
+
*/
|
|
79
|
+
export function applyDecisions(decisions, service, logger = null) {
|
|
80
|
+
let applied = 0;
|
|
81
|
+
for (const [i, d] of decisions.entries()) {
|
|
82
|
+
try {
|
|
83
|
+
if (d.action === "keep") continue;
|
|
84
|
+
if (d.action === "archive") {
|
|
85
|
+
for (const id of d.ids) {
|
|
86
|
+
const mem = service.getById(id);
|
|
87
|
+
if (mem && !mem.archived) { service.setArchived(id, true); applied++; }
|
|
88
|
+
}
|
|
89
|
+
} else if (d.action === "merge") {
|
|
90
|
+
const keeper = service.getById(d.keepSource);
|
|
91
|
+
if (!keeper || keeper.archived) continue;
|
|
92
|
+
service.update(d.keepSource, {
|
|
93
|
+
title: d.title,
|
|
94
|
+
content: d.content,
|
|
95
|
+
importance: d.importance ?? Math.max(keeper.importance, ...d.ids.map((id) => service.getById(id)?.importance ?? 1))
|
|
96
|
+
});
|
|
97
|
+
for (const id of d.ids) {
|
|
98
|
+
if (id !== d.keepSource) {
|
|
99
|
+
const mem = service.getById(id);
|
|
100
|
+
if (mem && !mem.archived) { service.setArchived(id, true); }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
applied++;
|
|
104
|
+
} else if (d.action === "conflict") {
|
|
105
|
+
const winner = service.getById(d.winner);
|
|
106
|
+
const loser = service.getById(d.loser);
|
|
107
|
+
if (!winner || !loser) continue;
|
|
108
|
+
service.update(d.winner, {
|
|
109
|
+
content: `${winner.content}\n\n(已否决旧信息:${[...loser.content].slice(0, 100).join("")})`
|
|
110
|
+
});
|
|
111
|
+
service.setArchived(d.loser, true);
|
|
112
|
+
applied++;
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// Skip individual bad decision; never corrupt the store. The optional
|
|
116
|
+
// logger makes the failure visible instead of failing silently.
|
|
117
|
+
logger?.warn?.(`dsh-mneme dream: failed to apply ${d.action} at index ${i}: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return applied;
|
|
121
|
+
}
|