@modusensus/dsh-mneme 0.1.1 → 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/src/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
+ });
@@ -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
+ }