@modusensus/dsh-mneme 0.1.6 → 0.2.1
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/LICENSE +21 -21
- package/README.md +239 -207
- package/cordis.patch.yml +15 -15
- package/lib/api.js +256 -245
- package/lib/client.js +461 -461
- package/lib/commands.js +64 -64
- package/lib/config.js +55 -15
- package/lib/dream/clustering.js +118 -0
- package/lib/dream/decisions.js +177 -121
- package/lib/dream.js +467 -209
- package/lib/embedding.js +97 -97
- package/lib/index.js +176 -120
- package/lib/inject.js +47 -47
- package/lib/local-embedder.js +265 -0
- package/lib/mirror.js +131 -131
- package/lib/reranker.js +210 -0
- package/lib/service.js +324 -174
- package/lib/settings.js +142 -142
- package/lib/store.js +466 -310
- package/lib/summarize.js +171 -171
- package/lib/tools.js +230 -241
- package/lib/vector-index.js +106 -0
- package/package.json +7 -3
- package/src/api.js +256 -245
- package/src/commands.js +64 -64
- package/src/config.js +55 -15
- package/src/dream/clustering.js +118 -0
- package/src/dream/decisions.js +177 -121
- package/src/dream.js +467 -209
- package/src/embedding.js +97 -97
- package/src/index.js +176 -120
- package/src/inject.js +47 -47
- package/src/local-embedder.js +265 -0
- package/src/mirror.js +131 -131
- package/src/reranker.js +210 -0
- package/src/service.js +324 -174
- package/src/settings.js +142 -142
- package/src/store.js +466 -310
- package/src/summarize.js +171 -171
- package/src/tools.js +230 -241
- package/src/vector-index.js +106 -0
package/lib/service.js
CHANGED
|
@@ -1,174 +1,324 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const INJECT_TYPES = new Set(["preference", "project", "decision", "summary"]);
|
|
4
|
+
|
|
5
|
+
export function createService({ store, mirror, config, onWrite }) {
|
|
6
|
+
// Optional dream scheduler hook, installed via setDreamHook after creation
|
|
7
|
+
// (the scheduler holds a reference back to the service, so it cannot be
|
|
8
|
+
// passed in the constructor). Fired on the same write events as onWrite.
|
|
9
|
+
let dreamHook = null;
|
|
10
|
+
|
|
11
|
+
// Optional vector embedder, installed via setEmbedder after creation. After
|
|
12
|
+
// any content write it fire-and-forgets a re-embed of the row so vector
|
|
13
|
+
// search stays in sync; failures are swallowed inside the embedder.
|
|
14
|
+
let embedder = null;
|
|
15
|
+
let vectorIndex = null;
|
|
16
|
+
let reranker = null;
|
|
17
|
+
|
|
18
|
+
function scheduleEmbed(memory) {
|
|
19
|
+
if (embedder && memory?.id) {
|
|
20
|
+
try { embedder.schedule(memory); } catch { /* ignore */ }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cross-encoder rerank over a candidate list (best effort). Reranker
|
|
26
|
+
* failures degrade to the original candidate order — reranking is an
|
|
27
|
+
* accuracy upgrade, never a correctness gate.
|
|
28
|
+
*/
|
|
29
|
+
async function rerankCandidates(query, candidates, topK) {
|
|
30
|
+
if (!reranker || !candidates.length) return candidates.slice(0, topK);
|
|
31
|
+
try {
|
|
32
|
+
const scored = await reranker.rerank(query, candidates.map((c) => ({ id: c.id, title: c.title, content: c.content })));
|
|
33
|
+
if (!Array.isArray(scored)) return candidates.slice(0, topK);
|
|
34
|
+
const byId = new Map(candidates.map((c) => [c.id, c]));
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const s of scored) {
|
|
37
|
+
const c = byId.get(s.id);
|
|
38
|
+
if (c) { out.push({ ...c, score: s.score }); if (out.length >= topK) break; }
|
|
39
|
+
}
|
|
40
|
+
return out.length ? out : candidates.slice(0, topK);
|
|
41
|
+
} catch {
|
|
42
|
+
return candidates.slice(0, topK);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Semantic-aware memory search: keyword recall (store.search) plus optional
|
|
48
|
+
* vector recall + rerank. mode:
|
|
49
|
+
* auto (default) keyword first, vector fills remaining slots (legacy)
|
|
50
|
+
* hybrid vector first, keyword fills remaining slots
|
|
51
|
+
* vector vector only, falls back to keyword when unavailable
|
|
52
|
+
* keyword text only, never touches the embedder
|
|
53
|
+
* useRerank runs the cross-encoder over the merged list when a reranker is
|
|
54
|
+
* installed; results carry an extra `score` when reranked.
|
|
55
|
+
*/
|
|
56
|
+
// Weighted blend factor for hybrid search; exposed so callers can tune it.
|
|
57
|
+
const DEFAULT_HYBRID_WEIGHTS = { vector: 0.6, keyword: 0.4 };
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Give a keyword-hit row a relevance score in [0,1]: title hits score
|
|
61
|
+
* higher than content hits, then scaled by importance (1-5). This lets
|
|
62
|
+
* keyword results participate in weighted hybrid blends.
|
|
63
|
+
*/
|
|
64
|
+
function scoreKeyword(row, q) {
|
|
65
|
+
const ql = q.toLowerCase();
|
|
66
|
+
const title = (row.title ?? "").toLowerCase();
|
|
67
|
+
const content = (row.content ?? "").toLowerCase();
|
|
68
|
+
const titleHit = title.includes(ql);
|
|
69
|
+
const base = titleHit ? 1 : content.includes(ql) ? 0.6 : 0.3;
|
|
70
|
+
return base * (0.5 + (row.importance ?? 3) / 10);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function searchMemories(query, { mode = "auto", topK = 20, threshold, useRerank = true } = {}) {
|
|
74
|
+
const q = String(query ?? "").trim();
|
|
75
|
+
if (!q) return [];
|
|
76
|
+
const lim = topK > 0 ? topK : 20;
|
|
77
|
+
|
|
78
|
+
// Keyword results, decorated with a score so they can be weight-blended
|
|
79
|
+
// with vector results and reported uniformly.
|
|
80
|
+
const rawKeyword = store.search(q, { limit: lim });
|
|
81
|
+
const keyword = rawKeyword.map((m) => ({ ...m, score: scoreKeyword(m, q) }));
|
|
82
|
+
const wantVector = mode === "vector" || mode === "hybrid" || (mode === "auto" && !!embedder);
|
|
83
|
+
let vector = [];
|
|
84
|
+
if (wantVector && embedder) {
|
|
85
|
+
try {
|
|
86
|
+
// Legacy embedders expose embed(query); local ones expose embedSingle.
|
|
87
|
+
const embedSingle = typeof embedder.embedSingle === "function"
|
|
88
|
+
? embedder.embedSingle.bind(embedder)
|
|
89
|
+
: embedder.embed.bind(embedder);
|
|
90
|
+
const qv = await embedSingle(q);
|
|
91
|
+
if (qv?.length) {
|
|
92
|
+
const hits = vectorIndex
|
|
93
|
+
? vectorIndex.search(qv, { limit: lim * 2, threshold: threshold ?? 0 })
|
|
94
|
+
: store.searchVector(qv, { limit: lim * 2, threshold: threshold ?? 0 });
|
|
95
|
+
vector = hits.map((m) => ({ ...m, vector: true }));
|
|
96
|
+
}
|
|
97
|
+
} catch { /* vector unavailable: keep keyword results */ }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Hybrid blending weights from config when provided.
|
|
101
|
+
const wv = config?.hybridSearchVectorWeight ?? DEFAULT_HYBRID_WEIGHTS.vector;
|
|
102
|
+
const wk = config?.hybridSearchKeywordWeight ?? DEFAULT_HYBRID_WEIGHTS.keyword;
|
|
103
|
+
|
|
104
|
+
let merged;
|
|
105
|
+
if (mode === "keyword") {
|
|
106
|
+
merged = keyword;
|
|
107
|
+
} else if (mode === "vector" || mode === "hybrid") {
|
|
108
|
+
// semantic-first: vector recalls lead, keyword fills remaining slots.
|
|
109
|
+
// Weighted blend when both sides scored the same memory; otherwise
|
|
110
|
+
// vector order leads (it is the semantic signal), keyword backfills.
|
|
111
|
+
const byId = new Map();
|
|
112
|
+
for (const m of vector) {
|
|
113
|
+
const rec = byId.get(m.id);
|
|
114
|
+
byId.set(m.id, rec ? { ...rec, score: Math.max(rec.score ?? 0, m.score ?? 0) } : m);
|
|
115
|
+
}
|
|
116
|
+
for (const m of keyword) {
|
|
117
|
+
const rec = byId.get(m.id);
|
|
118
|
+
if (rec) {
|
|
119
|
+
// Same memory from both sides: blend the scores.
|
|
120
|
+
byId.set(m.id, { ...rec, score: (rec.score ?? 0) * wv + (m.score ?? 0) * wk });
|
|
121
|
+
} else {
|
|
122
|
+
byId.set(m.id, m);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const ranked = [...byId.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
126
|
+
merged = ranked.slice(0, lim);
|
|
127
|
+
if (merged.length < lim && !merged.length) {
|
|
128
|
+
// Vector unavailable entirely: fall back to plain keyword.
|
|
129
|
+
merged = keyword.slice(0, lim);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
// auto: keyword leads, vector fills remaining slots (legacy behavior)
|
|
133
|
+
merged = keyword.slice(0, lim);
|
|
134
|
+
const seen = new Set(merged.map((m) => m.id));
|
|
135
|
+
for (const m of vector) {
|
|
136
|
+
if (merged.length >= lim) break;
|
|
137
|
+
if (!seen.has(m.id)) { seen.add(m.id); merged.push(m); }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
merged = merged.slice(0, lim);
|
|
142
|
+
if (useRerank && reranker && merged.length) {
|
|
143
|
+
return rerankCandidates(q, merged, lim);
|
|
144
|
+
}
|
|
145
|
+
return merged;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Fire-and-forget write notification; errors are swallowed to keep write
|
|
150
|
+
* paths clean. The store mutation has already committed, so a throwing
|
|
151
|
+
* subscriber must not surface as a write failure. Archive/forget flags are
|
|
152
|
+
* state toggles, not content writes, so they never notify.
|
|
153
|
+
*/
|
|
154
|
+
function notifyWrite() {
|
|
155
|
+
if (onWrite) {
|
|
156
|
+
try { onWrite(); } catch { /* ignore */ }
|
|
157
|
+
}
|
|
158
|
+
if (dreamHook) {
|
|
159
|
+
try { dreamHook(); } catch { /* ignore */ }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Save a memory, merging into an existing one when title matches within the same type.
|
|
165
|
+
* @returns {{action: "created"|"merged", memory: object}}
|
|
166
|
+
*/
|
|
167
|
+
function saveWithDedupe(memory) {
|
|
168
|
+
const existing = store
|
|
169
|
+
.list({ type: memory.type, limit: 100 })
|
|
170
|
+
.find((m) => m.title.trim() === String(memory.title).trim());
|
|
171
|
+
if (existing) {
|
|
172
|
+
const merged = store.update(existing.id, {
|
|
173
|
+
content: memory.content ?? existing.content,
|
|
174
|
+
importance: memory.importance ?? existing.importance,
|
|
175
|
+
tags: memory.tags ?? existing.tags,
|
|
176
|
+
title: memory.title ?? existing.title
|
|
177
|
+
});
|
|
178
|
+
syncMirror();
|
|
179
|
+
notifyWrite();
|
|
180
|
+
scheduleEmbed(merged);
|
|
181
|
+
return { action: "merged", memory: merged };
|
|
182
|
+
}
|
|
183
|
+
const created = store.save({
|
|
184
|
+
type: memory.type,
|
|
185
|
+
title: memory.title,
|
|
186
|
+
content: memory.content,
|
|
187
|
+
tags: memory.tags ?? [],
|
|
188
|
+
importance: memory.importance ?? 3,
|
|
189
|
+
source: memory.source ?? "manual"
|
|
190
|
+
});
|
|
191
|
+
syncMirror();
|
|
192
|
+
notifyWrite();
|
|
193
|
+
scheduleEmbed(created);
|
|
194
|
+
return { action: "created", memory: created };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Candidate memories for automatic context injection:
|
|
199
|
+
* summaries first, then all preferences, then non-forgotten items with
|
|
200
|
+
* importance >= threshold. History is never auto-injected. Archived entries
|
|
201
|
+
* are excluded (store.list already filters them by default; the extra
|
|
202
|
+
* !m.archived check is kept as double insurance).
|
|
203
|
+
*/
|
|
204
|
+
function injectCandidates({ maxItems = 5, threshold = 3 } = {}) {
|
|
205
|
+
const items = store.list({ limit: 200, includeForgotten: false })
|
|
206
|
+
.filter((m) => !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten &&
|
|
207
|
+
(m.type === "summary" || m.type === "preference" || m.importance >= threshold))
|
|
208
|
+
.sort((a, b) => {
|
|
209
|
+
const pa = a.type === "summary" ? 0 : a.type === "preference" ? 1 : 2;
|
|
210
|
+
const pb = b.type === "summary" ? 0 : b.type === "preference" ? 1 : 2;
|
|
211
|
+
return pa - pb || b.importance - a.importance;
|
|
212
|
+
});
|
|
213
|
+
return items.slice(0, maxItems);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Merge human edits parsed from a mirror file back into the store.
|
|
218
|
+
* Only content/title are taken; structure fields stay machine-owned.
|
|
219
|
+
*/
|
|
220
|
+
function mergeHumanEdits(type, edits) {
|
|
221
|
+
let applied = 0;
|
|
222
|
+
for (const edit of edits) {
|
|
223
|
+
if (!edit.id) continue; // corrupt/malformed edit: skip it, keep merging the rest
|
|
224
|
+
const existing = store.getById(edit.id);
|
|
225
|
+
if (!existing || existing.type !== type) continue;
|
|
226
|
+
const patch = {};
|
|
227
|
+
if (typeof edit.title === "string" && edit.title.trim()) patch.title = edit.title.trim();
|
|
228
|
+
if (typeof edit.content === "string" && edit.content.trim()) patch.content = edit.content.trim();
|
|
229
|
+
if (Object.keys(patch).length) {
|
|
230
|
+
store.update(edit.id, patch);
|
|
231
|
+
applied++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (applied) {
|
|
235
|
+
syncMirror();
|
|
236
|
+
notifyWrite();
|
|
237
|
+
}
|
|
238
|
+
return applied;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function toApiList(rows) {
|
|
242
|
+
return rows.map((m) => ({
|
|
243
|
+
id: m.id,
|
|
244
|
+
type: m.type,
|
|
245
|
+
title: m.title,
|
|
246
|
+
content: m.content,
|
|
247
|
+
tags: m.tags,
|
|
248
|
+
importance: m.importance,
|
|
249
|
+
source: m.source,
|
|
250
|
+
created_at: m.created_at,
|
|
251
|
+
updated_at: m.updated_at
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Re-render the human-editable mirror after any store mutation. Only
|
|
257
|
+
* non-forgotten memories are mirrored: forgotten entries must not reach the
|
|
258
|
+
* human-editable file (a human "edit" could otherwise resurrect them).
|
|
259
|
+
*/
|
|
260
|
+
function syncMirror() {
|
|
261
|
+
if (mirror) mirror.sync(store.list({ limit: 500, includeForgotten: false }));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
saveWithDedupe,
|
|
266
|
+
injectCandidates,
|
|
267
|
+
mergeHumanEdits,
|
|
268
|
+
toApiList,
|
|
269
|
+
setDreamHook(fn) { dreamHook = fn; },
|
|
270
|
+
setEmbedder(emb) { embedder = emb; },
|
|
271
|
+
setVectorIndex(vi) { vectorIndex = vi; },
|
|
272
|
+
setReranker(rn) { reranker = rn; },
|
|
273
|
+
searchMemories,
|
|
274
|
+
// passthroughs used by tools and api layers; mutations keep the mirror in sync
|
|
275
|
+
search: (q, o) => store.search(q, o),
|
|
276
|
+
searchVector: (v, o) => store.searchVector(v, o),
|
|
277
|
+
embeddedCount: () => store.embeddedCount(),
|
|
278
|
+
list: (o) => store.list(o),
|
|
279
|
+
all: () => store.all(),
|
|
280
|
+
count: (type) => store.count(type),
|
|
281
|
+
getById: (id) => store.getById(id),
|
|
282
|
+
remove: (id) => {
|
|
283
|
+
store.remove(id);
|
|
284
|
+
syncMirror();
|
|
285
|
+
notifyWrite();
|
|
286
|
+
},
|
|
287
|
+
update: (id, p) => {
|
|
288
|
+
const old = store.getById(id);
|
|
289
|
+
const updated = store.update(id, p);
|
|
290
|
+
// Record a user correction (only when content actually changed and the
|
|
291
|
+
// reflection failure tracker is enabled): expected = what it became,
|
|
292
|
+
// actual = what it was before. Feeds later reflection/evolution passes.
|
|
293
|
+
if (old && updated && config.reflectionFailureTracking && old.content !== updated.content) {
|
|
294
|
+
store.saveFailure({
|
|
295
|
+
id: randomUUID(),
|
|
296
|
+
expected: updated.content,
|
|
297
|
+
actual: old.content,
|
|
298
|
+
failure_type: "user_correction",
|
|
299
|
+
memory_id: id
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
syncMirror();
|
|
303
|
+
notifyWrite();
|
|
304
|
+
scheduleEmbed(updated);
|
|
305
|
+
return updated;
|
|
306
|
+
},
|
|
307
|
+
setForget: (id, f) => {
|
|
308
|
+
const updated = store.setForget(id, f);
|
|
309
|
+
syncMirror();
|
|
310
|
+
return updated;
|
|
311
|
+
},
|
|
312
|
+
setArchived: (id, f) => {
|
|
313
|
+
const updated = store.setArchived(id, f);
|
|
314
|
+
syncMirror();
|
|
315
|
+
return updated;
|
|
316
|
+
},
|
|
317
|
+
// autoDream audit trail: passthroughs deliberately bypass write hooks —
|
|
318
|
+
// an audit write is bookkeeping, and notifyWrite would loop back into the
|
|
319
|
+
// dream scheduler that just recorded the run.
|
|
320
|
+
saveDreamRun: (run) => store.saveDreamRun(run),
|
|
321
|
+
getDreamRun: (id) => store.getDreamRun(id),
|
|
322
|
+
listDreamRuns: (opts) => store.listDreamRuns(opts)
|
|
323
|
+
};
|
|
324
|
+
}
|