@yeaft/webchat-agent 0.1.664 → 0.1.666
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/connection/message-router.js +1 -24
- package/package.json +1 -1
- package/unify/cli.js +5 -84
- package/unify/engine.js +22 -130
- package/unify/features/summary.js +15 -98
- package/unify/index.js +0 -2
- package/unify/memory/consolidate.js +10 -125
- package/unify/prompts.js +3 -19
- package/unify/session.js +1 -21
- package/unify/stop-hooks.js +8 -44
- package/unify/tools/index.js +0 -11
- package/unify/tools/web-search.js +216 -40
- package/unify/web-bridge.js +0 -98
- package/unify/memory/dream-shard.js +0 -722
- package/unify/memory/extract.js +0 -101
- package/unify/memory/layout.js +0 -358
- package/unify/memory/schema.js +0 -166
- package/unify/memory/shard-store.js +0 -373
- package/unify/memory/store.js +0 -578
- package/unify/memory/types.js +0 -139
- package/unify/memory/user-memory-store.js +0 -452
- package/unify/tools/memory-query.js +0 -134
- package/unify/tools/memory-read.js +0 -90
- package/unify/tools/memory-search.js +0 -140
- package/unify/tools/memory-trace.js +0 -135
- package/unify/tools/memory-write.js +0 -113
- package/unify/user-memory.js +0 -107
|
@@ -1,452 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* user-memory-store.js — R6 §Δ29 User-memory store + dream + profile builder.
|
|
3
|
-
*
|
|
4
|
-
* Wraps the R6 shard-store (task-334f) with user-specific semantics:
|
|
5
|
-
* - 5 shards: profile / preferences / projects / goals / relations
|
|
6
|
-
* - Storage path: ~/.yeaft/user/memory/
|
|
7
|
-
* - UserDreamJob: reuses dream-shard.js compact framework
|
|
8
|
-
* - buildUserProfile(): top-N recall for SEMI-DYNAMIC injection
|
|
9
|
-
*
|
|
10
|
-
* Hard constraints:
|
|
11
|
-
* - No VP/task memory imports (user memory is orthogonal)
|
|
12
|
-
* - Never throws from public API — best-effort with console.warn fallback
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { homedir } from 'os';
|
|
16
|
-
import { join } from 'path';
|
|
17
|
-
import { randomUUID } from 'crypto';
|
|
18
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
19
|
-
import { openMemoryShardStore } from './shard-store.js';
|
|
20
|
-
import { USER_SHARDS } from './schema.js';
|
|
21
|
-
import { scanShards, runCompactJob } from './dream-shard.js';
|
|
22
|
-
import { pickEffort } from '../effort.js';
|
|
23
|
-
|
|
24
|
-
/** Default storage root for user memory. */
|
|
25
|
-
export const USER_MEMORY_DIR = join(homedir(), '.yeaft', 'user', 'memory');
|
|
26
|
-
|
|
27
|
-
/** Maximum entries to include in the user_profile prompt segment. */
|
|
28
|
-
const PROFILE_TOP_N = 5;
|
|
29
|
-
|
|
30
|
-
/** Shard priority for profile builder recall (most → least relevant). */
|
|
31
|
-
const PROFILE_RECALL_SHARDS = ['profile', 'preferences', 'goals'];
|
|
32
|
-
|
|
33
|
-
// ─── Lazy singleton ───────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
/** @type {ReturnType<typeof openMemoryShardStore> | null} */
|
|
36
|
-
let _store = null;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Get (or lazily create) the process-singleton user-memory shard store.
|
|
40
|
-
* Returns null on failure (missing dir, permissions, etc.) — caller must
|
|
41
|
-
* handle null gracefully.
|
|
42
|
-
*
|
|
43
|
-
* @param {{ dir?: string }} [opts]
|
|
44
|
-
* @returns {ReturnType<typeof openMemoryShardStore> | null}
|
|
45
|
-
*/
|
|
46
|
-
export function getUserMemoryStore(opts = {}) {
|
|
47
|
-
if (_store) return _store;
|
|
48
|
-
try {
|
|
49
|
-
const dir = opts.dir || USER_MEMORY_DIR;
|
|
50
|
-
_store = openMemoryShardStore(dir, 'user');
|
|
51
|
-
return _store;
|
|
52
|
-
} catch (err) {
|
|
53
|
-
console.warn('[user-memory-store] failed to open store:', err.message);
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Close and reset the singleton. Used by tests.
|
|
60
|
-
*/
|
|
61
|
-
export function _resetUserMemoryStoreForTest() {
|
|
62
|
-
if (_store) {
|
|
63
|
-
try { _store.close(); } catch { /* ignore */ }
|
|
64
|
-
}
|
|
65
|
-
_store = null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Open a fresh (non-singleton) user-memory store at an arbitrary dir.
|
|
70
|
-
* Useful for tests that want isolation.
|
|
71
|
-
*/
|
|
72
|
-
export function openUserMemoryStore(dir) {
|
|
73
|
-
return openMemoryShardStore(dir, 'user');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ─── Write / Remove ──────────────────────────────────────────
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Classify user-memory text into a shard based on simple heuristics.
|
|
80
|
-
* Falls back to 'profile' when uncertain.
|
|
81
|
-
*
|
|
82
|
-
* @param {string} text
|
|
83
|
-
* @param {string[]} [tags]
|
|
84
|
-
* @returns {string}
|
|
85
|
-
*/
|
|
86
|
-
export function classifyUserMemoryShard(text, tags) {
|
|
87
|
-
const lower = (text || '').toLowerCase();
|
|
88
|
-
const tagSet = new Set((tags || []).map(t => t.toLowerCase()));
|
|
89
|
-
|
|
90
|
-
// Explicit tag hints
|
|
91
|
-
if (tagSet.has('goal') || tagSet.has('goals')) return 'goals';
|
|
92
|
-
if (tagSet.has('project') || tagSet.has('projects')) return 'projects';
|
|
93
|
-
if (tagSet.has('preference') || tagSet.has('preferences')) return 'preferences';
|
|
94
|
-
if (tagSet.has('relation') || tagSet.has('relations')) return 'relations';
|
|
95
|
-
if (tagSet.has('profile')) return 'profile';
|
|
96
|
-
|
|
97
|
-
// Keyword heuristics
|
|
98
|
-
if (/\b(goal|objective|target|aim|aspir|want to|plan to|hope to)\b/i.test(lower)) return 'goals';
|
|
99
|
-
if (/\b(project|repo|codebase|app|application|product)\b/i.test(lower)) return 'projects';
|
|
100
|
-
if (/\b(prefer|like|dislike|style|format|tone|language|dark mode|theme)\b/i.test(lower)) return 'preferences';
|
|
101
|
-
if (/\b(colleague|friend|team|manager|report|partner|contact|person)\b/i.test(lower)) return 'relations';
|
|
102
|
-
|
|
103
|
-
return 'profile';
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Ingest a user-memory write. Returns the entryId on success, null on failure.
|
|
108
|
-
*
|
|
109
|
-
* @param {object} store — user-memory shard store
|
|
110
|
-
* @param {{ text: string, tags?: string[], sourceRef?: object }} params
|
|
111
|
-
* @returns {string|null} entryId
|
|
112
|
-
*/
|
|
113
|
-
export function writeUserMemory(store, { text, tags, sourceRef }) {
|
|
114
|
-
if (!store || !text || typeof text !== 'string' || !text.trim()) return null;
|
|
115
|
-
try {
|
|
116
|
-
const shard = classifyUserMemoryShard(text, tags);
|
|
117
|
-
const id = `um-${randomUUID().slice(0, 12)}`;
|
|
118
|
-
const entry = {
|
|
119
|
-
id,
|
|
120
|
-
shard,
|
|
121
|
-
kind: 'preference', // user-memory entries are preference-kind (no sourceRef required)
|
|
122
|
-
body: text.trim(),
|
|
123
|
-
tags: Array.isArray(tags) ? tags.slice() : [],
|
|
124
|
-
authoredBy: 'user:self',
|
|
125
|
-
};
|
|
126
|
-
store.put(entry);
|
|
127
|
-
return id;
|
|
128
|
-
} catch (err) {
|
|
129
|
-
console.warn('[user-memory-store] write failed:', err.message);
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Remove a user-memory entry by id. Returns true on success.
|
|
136
|
-
*
|
|
137
|
-
* @param {object} store
|
|
138
|
-
* @param {string} entryId
|
|
139
|
-
* @returns {boolean}
|
|
140
|
-
*/
|
|
141
|
-
export function removeUserMemory(store, entryId) {
|
|
142
|
-
if (!store || !entryId) return false;
|
|
143
|
-
try {
|
|
144
|
-
store.remove(entryId);
|
|
145
|
-
return true;
|
|
146
|
-
} catch (err) {
|
|
147
|
-
console.warn('[user-memory-store] remove failed:', err.message);
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ─── Profile Builder ─────────────────────────────────────────
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Build the `user_profile` text segment for SEMI-DYNAMIC prompt injection.
|
|
156
|
-
* Reads top-N entries from profile/preferences/goals shards and formats
|
|
157
|
-
* them as a compact bullet list.
|
|
158
|
-
*
|
|
159
|
-
* @param {object} [store] — user-memory shard store (uses singleton if omitted)
|
|
160
|
-
* @param {{ maxEntries?: number }} [opts]
|
|
161
|
-
* @returns {string} — empty string if no user-memory exists
|
|
162
|
-
*/
|
|
163
|
-
export function buildUserProfile(store, opts = {}) {
|
|
164
|
-
const s = store || getUserMemoryStore();
|
|
165
|
-
if (!s) return '';
|
|
166
|
-
|
|
167
|
-
const max = opts.maxEntries || PROFILE_TOP_N;
|
|
168
|
-
const lines = [];
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
for (const shardName of PROFILE_RECALL_SHARDS) {
|
|
172
|
-
if (lines.length >= max) break;
|
|
173
|
-
const { results } = s.query({ shard: shardName });
|
|
174
|
-
// Filter out superseded entries
|
|
175
|
-
const live = results.filter(r => !r.supersededBy);
|
|
176
|
-
// Take most recent first (results are already ordered by storage)
|
|
177
|
-
for (const rec of live) {
|
|
178
|
-
if (lines.length >= max) break;
|
|
179
|
-
const full = s.get(rec.id);
|
|
180
|
-
if (!full || !full.body) continue;
|
|
181
|
-
const body = full.body.trim();
|
|
182
|
-
if (!body) continue;
|
|
183
|
-
lines.push(`- ${body}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
} catch (err) {
|
|
187
|
-
console.warn('[user-memory-store] buildUserProfile failed:', err.message);
|
|
188
|
-
return '';
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return lines.join('\n');
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ─── Dream Job ───────────────────────────────────────────────
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Run user-memory dream maintenance: extract phase + compact.
|
|
198
|
-
* Extract reads conversation messages since the last watermark, uses LLM to
|
|
199
|
-
* identify user-relevant facts, then writes them to the appropriate shards.
|
|
200
|
-
* Compact phase reclaims superseded/removed tombstones (unchanged from 334g).
|
|
201
|
-
*
|
|
202
|
-
* @param {{
|
|
203
|
-
* store?: object,
|
|
204
|
-
* conversationStore?: object,
|
|
205
|
-
* adapter?: object,
|
|
206
|
-
* config?: object,
|
|
207
|
-
* onPhase?: (phase: string, data: any) => void,
|
|
208
|
-
* }} [opts]
|
|
209
|
-
* @returns {Promise<{ extract: object|null, scan: object, compact: object } | null>}
|
|
210
|
-
*/
|
|
211
|
-
export async function runUserDreamJob(opts = {}) {
|
|
212
|
-
const store = 'store' in opts ? opts.store : getUserMemoryStore();
|
|
213
|
-
if (!store) return null;
|
|
214
|
-
|
|
215
|
-
let extractResult = null;
|
|
216
|
-
|
|
217
|
-
try {
|
|
218
|
-
// ── Phase 1: Extract (LLM) ─────────────────────────────
|
|
219
|
-
if (opts.conversationStore && opts.adapter && opts.config) {
|
|
220
|
-
opts.onPhase?.('extract', 'starting');
|
|
221
|
-
try {
|
|
222
|
-
extractResult = await dreamExtract({
|
|
223
|
-
store,
|
|
224
|
-
conversationStore: opts.conversationStore,
|
|
225
|
-
adapter: opts.adapter,
|
|
226
|
-
config: opts.config,
|
|
227
|
-
});
|
|
228
|
-
opts.onPhase?.('extract', extractResult);
|
|
229
|
-
} catch (err) {
|
|
230
|
-
console.warn('[user-memory-store] extract phase failed:', err.message);
|
|
231
|
-
extractResult = { error: err.message, extracted: 0 };
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// ── Phase 2: Compact ───────────────────────────────────
|
|
236
|
-
const scan = scanShards(store);
|
|
237
|
-
const compact = runCompactJob({
|
|
238
|
-
shardStore: store,
|
|
239
|
-
shardNames: scan.needsCompaction,
|
|
240
|
-
onCompact: opts.onPhase
|
|
241
|
-
? (shard, r) => opts.onPhase('compact', { shard, ...r })
|
|
242
|
-
: undefined,
|
|
243
|
-
});
|
|
244
|
-
return {
|
|
245
|
-
extract: extractResult,
|
|
246
|
-
scan: { totalEntries: scan.totalEntries, totalBytes: scan.totalBytes },
|
|
247
|
-
compact,
|
|
248
|
-
};
|
|
249
|
-
} catch (err) {
|
|
250
|
-
console.warn('[user-memory-store] dream job failed:', err.message);
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ─── Watermark ──────────────────────────────────────────────
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Watermark format (shared with 334-w7b):
|
|
259
|
-
* { lastMessageId: string, lastMessageTs: number, updatedAt: string }
|
|
260
|
-
*
|
|
261
|
-
* Stored at <storeDir>/.watermark.json (alongside shard files).
|
|
262
|
-
*/
|
|
263
|
-
|
|
264
|
-
const WATERMARK_FILE = '.watermark.json';
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Read the extract watermark for a user-memory store.
|
|
268
|
-
* Returns null if no watermark exists yet.
|
|
269
|
-
*
|
|
270
|
-
* @param {string} dir — store directory (e.g. ~/.yeaft/user/memory)
|
|
271
|
-
* @returns {{ lastMessageId: string, lastMessageTs: number, updatedAt: string } | null}
|
|
272
|
-
*/
|
|
273
|
-
export function readWatermark(dir) {
|
|
274
|
-
try {
|
|
275
|
-
const p = join(dir, WATERMARK_FILE);
|
|
276
|
-
if (!existsSync(p)) return null;
|
|
277
|
-
return JSON.parse(readFileSync(p, 'utf8'));
|
|
278
|
-
} catch {
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Write the extract watermark.
|
|
285
|
-
*
|
|
286
|
-
* @param {string} dir
|
|
287
|
-
* @param {{ lastMessageId: string, lastMessageTs: number }} wm
|
|
288
|
-
*/
|
|
289
|
-
export function writeWatermark(dir, wm) {
|
|
290
|
-
try {
|
|
291
|
-
const p = join(dir, WATERMARK_FILE);
|
|
292
|
-
mkdirSync(dir, { recursive: true });
|
|
293
|
-
writeFileSync(p, JSON.stringify({
|
|
294
|
-
lastMessageId: wm.lastMessageId,
|
|
295
|
-
lastMessageTs: wm.lastMessageTs,
|
|
296
|
-
updatedAt: new Date().toISOString(),
|
|
297
|
-
}, null, 2));
|
|
298
|
-
} catch (err) {
|
|
299
|
-
console.warn('[user-memory-store] writeWatermark failed:', err.message);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// ─── Extract Phase ──────────────────────────────────────────
|
|
304
|
-
|
|
305
|
-
/** Max messages to process in a single extract pass. */
|
|
306
|
-
const EXTRACT_MAX_MESSAGES = 50;
|
|
307
|
-
|
|
308
|
-
/** Min messages required to trigger an extract. */
|
|
309
|
-
const EXTRACT_MIN_MESSAGES = 3;
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Build the user-memory extraction prompt.
|
|
313
|
-
* Tailored for user-relevant facts (not VP/task memory).
|
|
314
|
-
*
|
|
315
|
-
* @param {object[]} messages
|
|
316
|
-
* @returns {string}
|
|
317
|
-
*/
|
|
318
|
-
export function buildUserExtractPrompt(messages) {
|
|
319
|
-
const conversation = messages.map(m => {
|
|
320
|
-
const prefix = m.role === 'user' ? 'User' : m.role === 'assistant' ? 'Assistant' : 'System';
|
|
321
|
-
return `[${prefix}]: ${typeof m.content === 'string' ? m.content : JSON.stringify(m.content)}`;
|
|
322
|
-
}).join('\n\n');
|
|
323
|
-
|
|
324
|
-
return `Analyze the following conversation and extract facts about THE USER that are worth remembering long-term.
|
|
325
|
-
|
|
326
|
-
Focus on these categories:
|
|
327
|
-
- **profile**: Name, job title, company, location, background, expertise areas
|
|
328
|
-
- **preferences**: Coding style, tool preferences, language preferences, communication style
|
|
329
|
-
- **projects**: Projects they work on, tech stacks, repositories, products
|
|
330
|
-
- **goals**: Current goals, objectives, what they're trying to achieve
|
|
331
|
-
- **relations**: Team members, colleagues, managers, collaborators mentioned
|
|
332
|
-
|
|
333
|
-
For each fact, provide:
|
|
334
|
-
- **shard**: One of: profile, preferences, projects, goals, relations
|
|
335
|
-
- **body**: 1-2 sentences describing the fact clearly
|
|
336
|
-
- **tags**: 1-3 keyword tags as an array
|
|
337
|
-
|
|
338
|
-
Do NOT extract:
|
|
339
|
-
- Specific code snippets or technical instructions
|
|
340
|
-
- Temporary debugging context
|
|
341
|
-
- Facts about the assistant (only about the user)
|
|
342
|
-
- Information already implied by the conversation being about coding
|
|
343
|
-
|
|
344
|
-
Return a JSON array. If nothing about the user is worth remembering, return [].
|
|
345
|
-
|
|
346
|
-
Conversation:
|
|
347
|
-
${conversation}`;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Extract user-relevant facts from conversation messages and write to user-memory shards.
|
|
352
|
-
*
|
|
353
|
-
* @param {{
|
|
354
|
-
* store: object,
|
|
355
|
-
* conversationStore: object,
|
|
356
|
-
* adapter: object,
|
|
357
|
-
* config: object,
|
|
358
|
-
* dir?: string,
|
|
359
|
-
* }} params
|
|
360
|
-
* @returns {Promise<{ extracted: number, skipped: number, watermark: object|null }>}
|
|
361
|
-
*/
|
|
362
|
-
export async function dreamExtract({ store, conversationStore, adapter, config, dir }) {
|
|
363
|
-
const storeDir = dir || USER_MEMORY_DIR;
|
|
364
|
-
const wm = readWatermark(storeDir);
|
|
365
|
-
|
|
366
|
-
// Load all messages and filter to those after watermark
|
|
367
|
-
const allMessages = conversationStore.loadAll();
|
|
368
|
-
let newMessages;
|
|
369
|
-
|
|
370
|
-
if (wm && wm.lastMessageId) {
|
|
371
|
-
const idx = allMessages.findIndex(m => m.id === wm.lastMessageId);
|
|
372
|
-
newMessages = idx >= 0 ? allMessages.slice(idx + 1) : allMessages;
|
|
373
|
-
} else {
|
|
374
|
-
// No watermark — process all messages (first run)
|
|
375
|
-
newMessages = allMessages;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Filter to user + assistant messages only (skip system)
|
|
379
|
-
newMessages = newMessages.filter(m => m.role === 'user' || m.role === 'assistant');
|
|
380
|
-
|
|
381
|
-
if (newMessages.length < EXTRACT_MIN_MESSAGES) {
|
|
382
|
-
return { extracted: 0, skipped: 0, watermark: wm };
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Cap to prevent huge LLM calls
|
|
386
|
-
const batch = newMessages.slice(-EXTRACT_MAX_MESSAGES);
|
|
387
|
-
|
|
388
|
-
// LLM extraction call
|
|
389
|
-
const system = 'You are a user profile extraction assistant. Analyze conversations and extract facts about the user. Return ONLY a valid JSON array, no other text.';
|
|
390
|
-
const prompt = buildUserExtractPrompt(batch);
|
|
391
|
-
|
|
392
|
-
let candidates = [];
|
|
393
|
-
try {
|
|
394
|
-
const result = await adapter.call({
|
|
395
|
-
model: config.model || config.primaryModel || 'default',
|
|
396
|
-
system,
|
|
397
|
-
messages: [{ role: 'user', content: prompt }],
|
|
398
|
-
maxTokens: 2048,
|
|
399
|
-
effort: pickEffort({ scenario: 'dream' }),
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
const text = result.text.trim();
|
|
403
|
-
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
|
404
|
-
if (jsonMatch) {
|
|
405
|
-
candidates = JSON.parse(jsonMatch[0]);
|
|
406
|
-
}
|
|
407
|
-
} catch {
|
|
408
|
-
return { extracted: 0, skipped: 0, watermark: wm, error: 'llm_failed' };
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (!Array.isArray(candidates)) {
|
|
412
|
-
return { extracted: 0, skipped: 0, watermark: wm };
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Validate and write candidates
|
|
416
|
-
let extracted = 0;
|
|
417
|
-
let skipped = 0;
|
|
418
|
-
|
|
419
|
-
for (const c of candidates) {
|
|
420
|
-
if (!c || typeof c !== 'object' || !c.body) { skipped++; continue; }
|
|
421
|
-
|
|
422
|
-
// Use classifyUserMemoryShard if shard not provided or invalid
|
|
423
|
-
const shard = USER_SHARDS.includes(c.shard)
|
|
424
|
-
? c.shard
|
|
425
|
-
: classifyUserMemoryShard(c.body, c.tags);
|
|
426
|
-
|
|
427
|
-
const id = writeUserMemory(store, {
|
|
428
|
-
text: c.body,
|
|
429
|
-
tags: Array.isArray(c.tags) ? c.tags.map(String) : [],
|
|
430
|
-
sourceRef: { origin: 'dream-extract' },
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
if (id) {
|
|
434
|
-
extracted++;
|
|
435
|
-
} else {
|
|
436
|
-
skipped++;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Update watermark to last processed message
|
|
441
|
-
const lastMsg = batch[batch.length - 1];
|
|
442
|
-
if (lastMsg) {
|
|
443
|
-
const newWm = {
|
|
444
|
-
lastMessageId: lastMsg.id || '',
|
|
445
|
-
lastMessageTs: lastMsg.ts || Date.now(),
|
|
446
|
-
};
|
|
447
|
-
writeWatermark(storeDir, newWm);
|
|
448
|
-
return { extracted, skipped, watermark: newWm };
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return { extracted, skipped, watermark: wm };
|
|
452
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* memory-query.js — Search atomic memory entries by keywords/tags/scope.
|
|
3
|
-
*
|
|
4
|
-
* New-layout tool (task-287 memory refactor).
|
|
5
|
-
*
|
|
6
|
-
* Delegates to MemoryStore.findByFilter + MemoryStore.search for the actual
|
|
7
|
-
* heavy lifting; this is a tool-exposed wrapper that:
|
|
8
|
-
* 1. Accepts flat `keywords[]` (used as tags AND as content keyword scan)
|
|
9
|
-
* 2. Optional tags[], scope, limit
|
|
10
|
-
* 3. Returns a compact list suitable for LLM consumption
|
|
11
|
-
*
|
|
12
|
-
* Use this for fuzzy discovery over atomic entries. For loading a known
|
|
13
|
-
* classification file in full, use `memory_load` instead (renamed from
|
|
14
|
-
* the old `memory_search` in task-333b; the old name still works as a
|
|
15
|
-
* deprecated alias for one release).
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { defineTool } from './types.js';
|
|
19
|
-
|
|
20
|
-
const DEFAULT_LIMIT = 10;
|
|
21
|
-
const MAX_LIMIT = 30;
|
|
22
|
-
const SNIPPET_CHARS = 400;
|
|
23
|
-
|
|
24
|
-
export default defineTool({
|
|
25
|
-
name: 'memory_query',
|
|
26
|
-
description: `Search Yeaft's atomic memory entries by keywords, tags, and scope.
|
|
27
|
-
|
|
28
|
-
Scoring:
|
|
29
|
-
- Exact scope match: +3
|
|
30
|
-
- Ancestor/descendant scope: +2
|
|
31
|
-
- "global" scope (fallback): +1
|
|
32
|
-
- Each tag overlap: +1
|
|
33
|
-
- Keyword hit in entry content/name/tags: retained
|
|
34
|
-
|
|
35
|
-
Use this when the system-prompt Memory Index suggests the info is in atomic
|
|
36
|
-
entries (entries/) rather than in a classification file. Returns up to 'limit'
|
|
37
|
-
results sorted by score descending.`,
|
|
38
|
-
parameters: {
|
|
39
|
-
type: 'object',
|
|
40
|
-
properties: {
|
|
41
|
-
keywords: {
|
|
42
|
-
type: 'array',
|
|
43
|
-
items: { type: 'string' },
|
|
44
|
-
description: 'Words to search in entry content/name/tags. Required.',
|
|
45
|
-
},
|
|
46
|
-
tags: {
|
|
47
|
-
type: 'array',
|
|
48
|
-
items: { type: 'string' },
|
|
49
|
-
description: 'Exact-tag filter (scored separately from keywords).',
|
|
50
|
-
},
|
|
51
|
-
scope: {
|
|
52
|
-
type: 'string',
|
|
53
|
-
description: 'Memory scope to prefer (e.g. "work/my-project").',
|
|
54
|
-
},
|
|
55
|
-
limit: {
|
|
56
|
-
type: 'number',
|
|
57
|
-
description: `Max results (default ${DEFAULT_LIMIT}, max ${MAX_LIMIT})`,
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
required: ['keywords'],
|
|
61
|
-
},
|
|
62
|
-
isConcurrencySafe: () => true,
|
|
63
|
-
isReadOnly: () => true,
|
|
64
|
-
async execute(input, ctx) {
|
|
65
|
-
const memoryStore = ctx?.memoryStore;
|
|
66
|
-
if (!memoryStore) {
|
|
67
|
-
return JSON.stringify({ error: 'Memory system not initialized' });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const keywords = Array.isArray(input?.keywords)
|
|
71
|
-
? input.keywords.filter(k => typeof k === 'string' && k.trim())
|
|
72
|
-
: [];
|
|
73
|
-
if (keywords.length === 0) {
|
|
74
|
-
return JSON.stringify({ error: 'keywords is required and must be a non-empty string array' });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const tags = Array.isArray(input?.tags)
|
|
78
|
-
? input.tags.filter(t => typeof t === 'string' && t.trim())
|
|
79
|
-
: [];
|
|
80
|
-
const scope = typeof input?.scope === 'string' ? input.scope : undefined;
|
|
81
|
-
const rawLimit = Number.isFinite(input?.limit) ? input.limit : DEFAULT_LIMIT;
|
|
82
|
-
const limit = Math.max(1, Math.min(MAX_LIMIT, Math.floor(rawLimit)));
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
// Union tags: explicit tags[] + keywords (keywords double as tag hints)
|
|
86
|
-
const tagUnion = [...new Set([...tags, ...keywords])];
|
|
87
|
-
|
|
88
|
-
// Phase 1: scored filter by scope + tags
|
|
89
|
-
let results = memoryStore.findByFilter({
|
|
90
|
-
scope,
|
|
91
|
-
tags: tagUnion,
|
|
92
|
-
limit: limit * 3, // over-fetch for phase 2
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// Phase 2: if any entries lack tag overlap, augment with keyword full-text
|
|
96
|
-
// scan so rare-word queries still surface entries with matching content.
|
|
97
|
-
const seen = new Set(results.map(e => e.name));
|
|
98
|
-
for (const kw of keywords) {
|
|
99
|
-
if (results.length >= limit * 3) break;
|
|
100
|
-
const extra = memoryStore.search(kw, limit);
|
|
101
|
-
for (const e of extra) {
|
|
102
|
-
if (!seen.has(e.name)) {
|
|
103
|
-
seen.add(e.name);
|
|
104
|
-
results.push({ ...e, _score: (e._score || 0) + 0.5 });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Final sort + trim
|
|
110
|
-
results.sort((a, b) => (b._score || 0) - (a._score || 0));
|
|
111
|
-
results = results.slice(0, limit);
|
|
112
|
-
|
|
113
|
-
return JSON.stringify({
|
|
114
|
-
totalResults: results.length,
|
|
115
|
-
results: results.map(e => ({
|
|
116
|
-
name: e.name,
|
|
117
|
-
kind: e.kind,
|
|
118
|
-
scope: e.scope,
|
|
119
|
-
tags: e.tags || [],
|
|
120
|
-
importance: e.importance,
|
|
121
|
-
score: e._score,
|
|
122
|
-
snippet: e.content
|
|
123
|
-
? (e.content.length > SNIPPET_CHARS
|
|
124
|
-
? e.content.slice(0, SNIPPET_CHARS) + '…'
|
|
125
|
-
: e.content)
|
|
126
|
-
: '',
|
|
127
|
-
updated_at: e.updated_at,
|
|
128
|
-
})),
|
|
129
|
-
}, null, 2);
|
|
130
|
-
} catch (err) {
|
|
131
|
-
return JSON.stringify({ error: `memory_query failed: ${err.message}` });
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
});
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* memory-read.js — Read memory entries from the Yeaft memory store.
|
|
3
|
-
*
|
|
4
|
-
* Reads the user profile (MEMORY.md), specific sections, or individual
|
|
5
|
-
* memory entries by name.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { defineTool } from './types.js';
|
|
9
|
-
|
|
10
|
-
export default defineTool({
|
|
11
|
-
name: 'MemoryRead',
|
|
12
|
-
description: `Read from Yeaft's persistent memory system.
|
|
13
|
-
|
|
14
|
-
Actions:
|
|
15
|
-
- "profile" — read the full MEMORY.md user profile
|
|
16
|
-
- "section" — read a specific section from MEMORY.md (e.g. "Facts", "Preferences")
|
|
17
|
-
- "entry" — read a specific memory entry by name
|
|
18
|
-
- "list" — list all memory entries (frontmatter only, no body)
|
|
19
|
-
- "scopes" — list all memory scopes and their entry counts`,
|
|
20
|
-
parameters: {
|
|
21
|
-
type: 'object',
|
|
22
|
-
properties: {
|
|
23
|
-
action: {
|
|
24
|
-
type: 'string',
|
|
25
|
-
enum: ['profile', 'section', 'entry', 'list', 'scopes'],
|
|
26
|
-
description: 'What to read from memory',
|
|
27
|
-
},
|
|
28
|
-
name: {
|
|
29
|
-
type: 'string',
|
|
30
|
-
description: 'Entry name slug (for "entry" action) or section name (for "section" action)',
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
required: ['action'],
|
|
34
|
-
},
|
|
35
|
-
isConcurrencySafe: () => true,
|
|
36
|
-
isReadOnly: () => true,
|
|
37
|
-
async execute(input, ctx) {
|
|
38
|
-
const memoryStore = ctx?.memoryStore;
|
|
39
|
-
if (!memoryStore) {
|
|
40
|
-
return JSON.stringify({ error: 'Memory system not initialized' });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
switch (input.action) {
|
|
45
|
-
case 'profile': {
|
|
46
|
-
const profile = memoryStore.readProfile();
|
|
47
|
-
return profile || '(No profile found — MEMORY.md is empty)';
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
case 'section': {
|
|
51
|
-
if (!input.name) return JSON.stringify({ error: 'name is required for "section" action' });
|
|
52
|
-
const section = memoryStore.readSection(input.name);
|
|
53
|
-
return section || `(Section "${input.name}" not found in MEMORY.md)`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
case 'entry': {
|
|
57
|
-
if (!input.name) return JSON.stringify({ error: 'name is required for "entry" action' });
|
|
58
|
-
const entry = memoryStore.readEntry(input.name);
|
|
59
|
-
if (!entry) return JSON.stringify({ error: `Entry "${input.name}" not found` });
|
|
60
|
-
return JSON.stringify(entry, null, 2);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
case 'list': {
|
|
64
|
-
const entries = memoryStore.listEntries();
|
|
65
|
-
return JSON.stringify({
|
|
66
|
-
entries: entries.map(e => ({
|
|
67
|
-
name: e.name,
|
|
68
|
-
kind: e.kind,
|
|
69
|
-
scope: e.scope,
|
|
70
|
-
tags: e.tags,
|
|
71
|
-
importance: e.importance,
|
|
72
|
-
updated_at: e.updated_at,
|
|
73
|
-
})),
|
|
74
|
-
totalCount: entries.length,
|
|
75
|
-
}, null, 2);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
case 'scopes': {
|
|
79
|
-
const scopes = memoryStore.readScopes();
|
|
80
|
-
return JSON.stringify({ scopes }, null, 2);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
default:
|
|
84
|
-
return JSON.stringify({ error: `Unknown action: ${input.action}` });
|
|
85
|
-
}
|
|
86
|
-
} catch (err) {
|
|
87
|
-
return JSON.stringify({ error: `Memory read failed: ${err.message}` });
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
});
|