@yeaft/webchat-agent 0.1.665 → 0.1.667
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/config.js +2 -2
- package/unify/dream-v2/apply.js +1 -1
- package/unify/dream-v2/limits.js +1 -1
- package/unify/dream-v2/merge.js +1 -1
- package/unify/dream-v2/runner.js +2 -2
- package/unify/dream-v2/schedule.js +2 -2
- package/unify/dream-v2/segment.js +1 -1
- package/unify/dream-v2/session-wiring.js +2 -3
- package/unify/dream-v2/snapshot.js +1 -1
- package/unify/dream-v2/state.js +1 -1
- package/unify/dream-v2/triage.js +1 -1
- package/unify/engine.js +23 -133
- package/unify/eval/cases/memory.js +9 -142
- package/unify/features/summary.js +15 -98
- package/unify/index.js +0 -2
- package/unify/memory/ams.js +1 -1
- package/unify/memory/consolidate.js +10 -125
- package/unify/memory/segment-store.js +1 -1
- package/unify/memory/store-v2.js +6 -9
- package/unify/prompts.js +8 -50
- package/unify/session.js +2 -22
- package/unify/stop-hooks.js +8 -44
- package/unify/tools/index.js +0 -11
- 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
package/unify/memory/extract.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* extract.js — Extract memory-worthy entries from conversation
|
|
3
|
-
*
|
|
4
|
-
* Called by consolidate.js during the Consolidate lifecycle.
|
|
5
|
-
* Uses a single LLM call to identify facts, preferences, skills,
|
|
6
|
-
* lessons, contexts, and relations from conversation messages.
|
|
7
|
-
*
|
|
8
|
-
* Reference: yeaft-unify-core-systems.md §3.1, yeaft-unify-design.md §6.1
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { MEMORY_KINDS } from './store.js';
|
|
12
|
-
import { pickEffort } from '../effort.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Build the extraction prompt.
|
|
16
|
-
* @param {object[]} messages — conversation messages to analyze
|
|
17
|
-
* @returns {string}
|
|
18
|
-
*/
|
|
19
|
-
function buildExtractionPrompt(messages) {
|
|
20
|
-
const conversation = messages.map(m => {
|
|
21
|
-
const prefix = m.role === 'user' ? 'User' : m.role === 'assistant' ? 'Assistant' : 'System';
|
|
22
|
-
return `[${prefix}]: ${m.content}`;
|
|
23
|
-
}).join('\n\n');
|
|
24
|
-
|
|
25
|
-
return `Analyze the following conversation and extract any memorable information worth saving to long-term memory.
|
|
26
|
-
|
|
27
|
-
For each memory, provide:
|
|
28
|
-
- **name**: A short slug-friendly name (e.g., "user-prefers-typescript", "project-uses-vue3")
|
|
29
|
-
- **kind**: One of: ${MEMORY_KINDS.join(', ')}
|
|
30
|
-
- **scope**: A tree path (e.g., "global", "tech/typescript", "work/project-name")
|
|
31
|
-
- **tags**: Relevant keywords as an array
|
|
32
|
-
- **importance**: "high", "normal", or "low"
|
|
33
|
-
- **content**: 1-3 sentences describing the memory
|
|
34
|
-
|
|
35
|
-
Memory kinds explained:
|
|
36
|
-
- fact: Objective facts (project structure, tech stack)
|
|
37
|
-
- preference: User preferences (coding style, tools)
|
|
38
|
-
- skill: How to do something (patterns, techniques)
|
|
39
|
-
- lesson: Lessons learned (bugs, pitfalls)
|
|
40
|
-
- context: Temporal context (current OKR, progress)
|
|
41
|
-
- relation: People and relationships (teammates, roles)
|
|
42
|
-
|
|
43
|
-
Do NOT extract:
|
|
44
|
-
- Specific code snippets (too large, will become stale)
|
|
45
|
-
- Temporary debugging information
|
|
46
|
-
- Trivial greetings or small talk
|
|
47
|
-
|
|
48
|
-
Return a JSON array of memory objects. If nothing is worth remembering, return an empty array [].
|
|
49
|
-
|
|
50
|
-
Conversation:
|
|
51
|
-
${conversation}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Extract memory entries from a set of conversation messages.
|
|
56
|
-
*
|
|
57
|
-
* @param {{ messages: object[], adapter: object, config: object }} params
|
|
58
|
-
* @returns {Promise<object[]>} — extracted memory entries
|
|
59
|
-
*/
|
|
60
|
-
export async function extractMemories({ messages, adapter, config }) {
|
|
61
|
-
if (!messages || messages.length === 0) return [];
|
|
62
|
-
|
|
63
|
-
const system = 'You are a memory extraction assistant. Analyze conversations and extract important facts, preferences, and lessons. Return ONLY a valid JSON array, no other text.';
|
|
64
|
-
|
|
65
|
-
const extractionPrompt = buildExtractionPrompt(messages);
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
const result = await adapter.call({
|
|
69
|
-
model: config.model,
|
|
70
|
-
system,
|
|
71
|
-
messages: [{ role: 'user', content: extractionPrompt }],
|
|
72
|
-
maxTokens: 2048,
|
|
73
|
-
// task-327c: extract runs inside the consolidate pipeline — the
|
|
74
|
-
// JSON-structured output benefits from the same 'max' thinking tier.
|
|
75
|
-
effort: pickEffort({ scenario: 'consolidate' }),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const text = result.text.trim();
|
|
79
|
-
|
|
80
|
-
// Try to parse JSON array from the response
|
|
81
|
-
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
|
82
|
-
if (!jsonMatch) return [];
|
|
83
|
-
|
|
84
|
-
const entries = JSON.parse(jsonMatch[0]);
|
|
85
|
-
|
|
86
|
-
// Validate and normalize entries
|
|
87
|
-
return entries
|
|
88
|
-
.filter(e => e && typeof e === 'object' && e.name && e.content)
|
|
89
|
-
.map(e => ({
|
|
90
|
-
name: String(e.name).slice(0, 80),
|
|
91
|
-
kind: MEMORY_KINDS.includes(e.kind) ? e.kind : 'fact',
|
|
92
|
-
scope: String(e.scope || 'global'),
|
|
93
|
-
tags: Array.isArray(e.tags) ? e.tags.map(String) : [],
|
|
94
|
-
importance: ['high', 'normal', 'low'].includes(e.importance) ? e.importance : 'normal',
|
|
95
|
-
content: String(e.content),
|
|
96
|
-
}));
|
|
97
|
-
} catch {
|
|
98
|
-
// LLM failure — return empty (non-critical operation)
|
|
99
|
-
return [];
|
|
100
|
-
}
|
|
101
|
-
}
|
package/unify/memory/layout.js
DELETED
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* layout.js — New-layout memory file helpers (tool-on-demand model)
|
|
3
|
-
*
|
|
4
|
-
* Directory layout:
|
|
5
|
-
* ~/.yeaft/memory/
|
|
6
|
-
* index.md — classification catalog (always injected into system prompt)
|
|
7
|
-
* user-preferences.md — merged user preferences (default-injected)
|
|
8
|
-
* entries/*.md — atomic entries (existing layout, read by memory_query)
|
|
9
|
-
* by-project/<slug>.md — per-project narrative summaries
|
|
10
|
-
* by-topic/<slug>.md — per-topic narrative summaries
|
|
11
|
-
* timeline/<YYYY-MM>.md — monthly narrative digests
|
|
12
|
-
*
|
|
13
|
-
* Design:
|
|
14
|
-
* - index.md is a single human-readable file listing all classification files
|
|
15
|
-
* with a one-line summary and entry count per section.
|
|
16
|
-
* - Aggregate files (by-project / by-topic / timeline) are narrative prose,
|
|
17
|
-
* not raw entry concat — produced by Dream.
|
|
18
|
-
* - user-preferences.md is a deduped accumulation of preferences extracted
|
|
19
|
-
* from conversations.
|
|
20
|
-
* - Project header match: basename(cwd) is matched against by-project/<slug>.md
|
|
21
|
-
* filenames (case-insensitive substring match either direction).
|
|
22
|
-
*
|
|
23
|
-
* This module lives alongside store.js; it does NOT replace MemoryStore.
|
|
24
|
-
* Old MEMORY.md / scopes.md continue to exist for backward compatibility
|
|
25
|
-
* but are no longer maintained by Dream after this refactor.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, statSync } from 'fs';
|
|
29
|
-
import { basename, join } from 'path';
|
|
30
|
-
import { isPermissionError } from '../init.js';
|
|
31
|
-
|
|
32
|
-
// ─── Constants ──────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
/** Classification category dirnames. */
|
|
35
|
-
export const CATEGORY_DIRS = ['by-project', 'by-topic', 'timeline'];
|
|
36
|
-
|
|
37
|
-
/** Classification single-file names living directly under memory/. */
|
|
38
|
-
export const SINGLE_FILES = ['index.md', 'user-preferences.md'];
|
|
39
|
-
|
|
40
|
-
/** Approximate character budget for prompt injection (~1.5k tokens). */
|
|
41
|
-
export const PROMPT_INJECTION_CHAR_BUDGET = 6000;
|
|
42
|
-
|
|
43
|
-
/** Character budget for project header excerpt (~300 tokens). */
|
|
44
|
-
export const PROJECT_HEADER_CHAR_BUDGET = 1200;
|
|
45
|
-
|
|
46
|
-
// ─── Path helpers ───────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @param {string} yeaftDir — e.g. ~/.yeaft
|
|
50
|
-
* @returns {string} — absolute path to memory root
|
|
51
|
-
*/
|
|
52
|
-
export function memoryDir(yeaftDir) {
|
|
53
|
-
return join(yeaftDir, 'memory');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Ensure the new-layout directory skeleton exists (idempotent).
|
|
58
|
-
* @param {string} yeaftDir
|
|
59
|
-
*/
|
|
60
|
-
export function ensureLayout(yeaftDir) {
|
|
61
|
-
const root = memoryDir(yeaftDir);
|
|
62
|
-
const dirs = [root, ...CATEGORY_DIRS.map(d => join(root, d)), join(root, 'entries')];
|
|
63
|
-
for (const d of dirs) {
|
|
64
|
-
try {
|
|
65
|
-
if (!existsSync(d)) mkdirSync(d, { recursive: true, mode: 0o755 });
|
|
66
|
-
} catch (err) {
|
|
67
|
-
if (!isPermissionError(err)) throw err;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ─── File I/O ───────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Read a file under memory/ by relative path. Returns '' if missing.
|
|
76
|
-
* @param {string} yeaftDir
|
|
77
|
-
* @param {string} relPath — e.g. 'index.md', 'by-project/foo.md'
|
|
78
|
-
* @returns {string}
|
|
79
|
-
*/
|
|
80
|
-
export function readMemoryFile(yeaftDir, relPath) {
|
|
81
|
-
const fp = join(memoryDir(yeaftDir), relPath);
|
|
82
|
-
if (!existsSync(fp)) return '';
|
|
83
|
-
try {
|
|
84
|
-
return readFileSync(fp, 'utf8');
|
|
85
|
-
} catch (err) {
|
|
86
|
-
if (isPermissionError(err)) return '';
|
|
87
|
-
throw err;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Write a file under memory/ by relative path. Creates parent dir if needed.
|
|
93
|
-
* @param {string} yeaftDir
|
|
94
|
-
* @param {string} relPath
|
|
95
|
-
* @param {string} content
|
|
96
|
-
*/
|
|
97
|
-
export function writeMemoryFile(yeaftDir, relPath, content) {
|
|
98
|
-
ensureLayout(yeaftDir);
|
|
99
|
-
const fp = join(memoryDir(yeaftDir), relPath);
|
|
100
|
-
try {
|
|
101
|
-
writeFileSync(fp, content, { encoding: 'utf8', mode: 0o644 });
|
|
102
|
-
} catch (err) {
|
|
103
|
-
if (!isPermissionError(err)) throw err;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* List all classification files (relative paths) that currently exist.
|
|
109
|
-
* Includes single files (index.md, user-preferences.md) and per-category files.
|
|
110
|
-
*
|
|
111
|
-
* @param {string} yeaftDir
|
|
112
|
-
* @returns {{ path: string, size: number }[]}
|
|
113
|
-
*/
|
|
114
|
-
export function listClassificationFiles(yeaftDir) {
|
|
115
|
-
const root = memoryDir(yeaftDir);
|
|
116
|
-
const out = [];
|
|
117
|
-
|
|
118
|
-
for (const f of SINGLE_FILES) {
|
|
119
|
-
const fp = join(root, f);
|
|
120
|
-
if (existsSync(fp)) {
|
|
121
|
-
try {
|
|
122
|
-
out.push({ path: f, size: statSync(fp).size });
|
|
123
|
-
} catch { /* ignore */ }
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
for (const dir of CATEGORY_DIRS) {
|
|
128
|
-
const dp = join(root, dir);
|
|
129
|
-
if (!existsSync(dp)) continue;
|
|
130
|
-
try {
|
|
131
|
-
for (const f of readdirSync(dp)) {
|
|
132
|
-
if (!f.endsWith('.md')) continue;
|
|
133
|
-
const fp = join(dp, f);
|
|
134
|
-
try {
|
|
135
|
-
out.push({ path: `${dir}/${f}`, size: statSync(fp).size });
|
|
136
|
-
} catch { /* ignore */ }
|
|
137
|
-
}
|
|
138
|
-
} catch { /* ignore */ }
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return out;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ─── Project header matching ────────────────────────────────
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Given a cwd, find the best-matching by-project/<slug>.md filename.
|
|
148
|
-
* Rule: case-insensitive substring match between basename(cwd) and slug
|
|
149
|
-
* (either direction). Returns the first match by sort order, or null.
|
|
150
|
-
*
|
|
151
|
-
* @param {string} yeaftDir
|
|
152
|
-
* @param {string} cwd
|
|
153
|
-
* @returns {string|null} — relative path like 'by-project/claude-web-chat.md', or null
|
|
154
|
-
*/
|
|
155
|
-
export function findProjectFile(yeaftDir, cwd) {
|
|
156
|
-
if (!cwd) return null;
|
|
157
|
-
const base = basename(cwd).toLowerCase();
|
|
158
|
-
if (!base) return null;
|
|
159
|
-
|
|
160
|
-
const dir = join(memoryDir(yeaftDir), 'by-project');
|
|
161
|
-
if (!existsSync(dir)) return null;
|
|
162
|
-
|
|
163
|
-
let files;
|
|
164
|
-
try {
|
|
165
|
-
files = readdirSync(dir).filter(f => f.endsWith('.md')).sort();
|
|
166
|
-
} catch {
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
for (const f of files) {
|
|
171
|
-
const slug = f.slice(0, -3).toLowerCase();
|
|
172
|
-
if (slug === base) return `by-project/${f}`;
|
|
173
|
-
}
|
|
174
|
-
for (const f of files) {
|
|
175
|
-
const slug = f.slice(0, -3).toLowerCase();
|
|
176
|
-
if (slug.includes(base) || base.includes(slug)) return `by-project/${f}`;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Return the leading excerpt of a file capped at charBudget.
|
|
184
|
-
* Trims to the last complete line boundary to avoid mid-line cutoff.
|
|
185
|
-
*
|
|
186
|
-
* @param {string} text
|
|
187
|
-
* @param {number} charBudget
|
|
188
|
-
* @returns {string}
|
|
189
|
-
*/
|
|
190
|
-
export function excerpt(text, charBudget) {
|
|
191
|
-
if (!text) return '';
|
|
192
|
-
if (text.length <= charBudget) return text;
|
|
193
|
-
const cut = text.slice(0, charBudget);
|
|
194
|
-
const lastNl = cut.lastIndexOf('\n');
|
|
195
|
-
return lastNl > charBudget * 0.6 ? cut.slice(0, lastNl) : cut;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ─── Index rendering ────────────────────────────────────────
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Render an auto-generated index.md from the current on-disk layout.
|
|
202
|
-
*
|
|
203
|
-
* Format:
|
|
204
|
-
* # Memory Index
|
|
205
|
-
*
|
|
206
|
-
* ## Single files
|
|
207
|
-
* - user-preferences.md (NNN bytes) — user-written/Dream-merged preferences
|
|
208
|
-
*
|
|
209
|
-
* ## by-project
|
|
210
|
-
* - by-project/foo.md (NNN bytes)
|
|
211
|
-
* - by-project/bar.md (NNN bytes)
|
|
212
|
-
*
|
|
213
|
-
* ## by-topic ...
|
|
214
|
-
* ## timeline ...
|
|
215
|
-
* ## entries
|
|
216
|
-
* - N atomic entries (use memory_query to search)
|
|
217
|
-
*
|
|
218
|
-
* One-line summaries are pulled from the first non-empty, non-heading line
|
|
219
|
-
* of each file.
|
|
220
|
-
*
|
|
221
|
-
* @param {string} yeaftDir
|
|
222
|
-
* @param {number} entryCount — number of atomic entries (from MemoryStore)
|
|
223
|
-
* @returns {string}
|
|
224
|
-
*/
|
|
225
|
-
export function renderIndex(yeaftDir, entryCount) {
|
|
226
|
-
const root = memoryDir(yeaftDir);
|
|
227
|
-
const lines = ['# Memory Index', ''];
|
|
228
|
-
|
|
229
|
-
// Single files section
|
|
230
|
-
const singleLines = [];
|
|
231
|
-
for (const f of SINGLE_FILES) {
|
|
232
|
-
if (f === 'index.md') continue;
|
|
233
|
-
const fp = join(root, f);
|
|
234
|
-
if (!existsSync(fp)) continue;
|
|
235
|
-
const summary = firstSummary(fp);
|
|
236
|
-
const size = safeSize(fp);
|
|
237
|
-
singleLines.push(`- ${f} (${size} bytes)${summary ? ` — ${summary}` : ''}`);
|
|
238
|
-
}
|
|
239
|
-
if (singleLines.length) {
|
|
240
|
-
lines.push('## Single files', '', ...singleLines, '');
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Category sections
|
|
244
|
-
for (const cat of CATEGORY_DIRS) {
|
|
245
|
-
const dp = join(root, cat);
|
|
246
|
-
if (!existsSync(dp)) continue;
|
|
247
|
-
let files;
|
|
248
|
-
try {
|
|
249
|
-
files = readdirSync(dp).filter(f => f.endsWith('.md')).sort();
|
|
250
|
-
} catch {
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
if (!files.length) continue;
|
|
254
|
-
lines.push(`## ${cat}`, '');
|
|
255
|
-
for (const f of files) {
|
|
256
|
-
const fp = join(dp, f);
|
|
257
|
-
const summary = firstSummary(fp);
|
|
258
|
-
const size = safeSize(fp);
|
|
259
|
-
lines.push(`- ${cat}/${f} (${size} bytes)${summary ? ` — ${summary}` : ''}`);
|
|
260
|
-
}
|
|
261
|
-
lines.push('');
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Entries section (count only — atomic entries are searched via memory_query)
|
|
265
|
-
lines.push('## entries', '', `- ${entryCount} atomic entries (use memory_query to search)`, '');
|
|
266
|
-
|
|
267
|
-
lines.push(
|
|
268
|
-
'_Note: use the `memory_load` tool with one or more paths to load a classification',
|
|
269
|
-
'file in full, or `memory_query` to search atomic entries by keywords/tags._',
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
return lines.join('\n') + '\n';
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* First non-heading non-empty line of a file, trimmed to 120 chars.
|
|
277
|
-
*/
|
|
278
|
-
function firstSummary(fp) {
|
|
279
|
-
try {
|
|
280
|
-
const raw = readFileSync(fp, 'utf8');
|
|
281
|
-
for (const line of raw.split('\n')) {
|
|
282
|
-
const s = line.trim();
|
|
283
|
-
if (!s) continue;
|
|
284
|
-
if (s.startsWith('#')) continue;
|
|
285
|
-
if (s.startsWith('---')) continue;
|
|
286
|
-
return s.length > 120 ? s.slice(0, 120) + '…' : s;
|
|
287
|
-
}
|
|
288
|
-
} catch { /* ignore */ }
|
|
289
|
-
return '';
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function safeSize(fp) {
|
|
293
|
-
try {
|
|
294
|
-
return statSync(fp).size;
|
|
295
|
-
} catch {
|
|
296
|
-
return 0;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ─── Prompt injection builder ───────────────────────────────
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Build the memory section to inject into the system prompt every turn.
|
|
304
|
-
*
|
|
305
|
-
* Content, in order:
|
|
306
|
-
* 1. index.md (full text — auto-regenerated when missing/stale)
|
|
307
|
-
* 2. user-preferences.md (full text)
|
|
308
|
-
* 3. Project header excerpt (first PROJECT_HEADER_CHAR_BUDGET chars of
|
|
309
|
-
* the matching by-project/<slug>.md, if cwd matches)
|
|
310
|
-
*
|
|
311
|
-
* Total output is capped at PROMPT_INJECTION_CHAR_BUDGET; later sections
|
|
312
|
-
* are dropped first if over budget.
|
|
313
|
-
*
|
|
314
|
-
* @param {{
|
|
315
|
-
* yeaftDir: string,
|
|
316
|
-
* cwd?: string,
|
|
317
|
-
* entryCount?: number,
|
|
318
|
-
* language?: 'en' | 'zh',
|
|
319
|
-
* }} params
|
|
320
|
-
* @returns {string}
|
|
321
|
-
*/
|
|
322
|
-
export function buildMemoryInjection({ yeaftDir, cwd, entryCount = 0, language = 'en' }) {
|
|
323
|
-
if (!yeaftDir) return '';
|
|
324
|
-
|
|
325
|
-
const heading = language === 'zh' ? '## 记忆索引' : '## Memory Index';
|
|
326
|
-
const prefHeading = language === 'zh' ? '## 用户偏好' : '## User Preferences';
|
|
327
|
-
const projectHeading = language === 'zh' ? '## 当前项目摘要' : '## Current Project Summary';
|
|
328
|
-
|
|
329
|
-
let indexText = readMemoryFile(yeaftDir, 'index.md');
|
|
330
|
-
if (!indexText.trim()) {
|
|
331
|
-
// Auto-generate an index on the fly so the LLM always sees something useful.
|
|
332
|
-
indexText = renderIndex(yeaftDir, entryCount);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const prefText = readMemoryFile(yeaftDir, 'user-preferences.md');
|
|
336
|
-
|
|
337
|
-
let projectText = '';
|
|
338
|
-
const projectRel = cwd ? findProjectFile(yeaftDir, cwd) : null;
|
|
339
|
-
if (projectRel) {
|
|
340
|
-
projectText = excerpt(readMemoryFile(yeaftDir, projectRel), PROJECT_HEADER_CHAR_BUDGET);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const sections = [];
|
|
344
|
-
sections.push(`${heading}\n${indexText.trim()}`);
|
|
345
|
-
if (prefText.trim()) sections.push(`${prefHeading}\n${prefText.trim()}`);
|
|
346
|
-
if (projectText.trim()) sections.push(`${projectHeading} (${projectRel})\n${projectText.trim()}`);
|
|
347
|
-
|
|
348
|
-
// Enforce total char budget — drop from the end.
|
|
349
|
-
let combined = sections.join('\n\n');
|
|
350
|
-
while (combined.length > PROMPT_INJECTION_CHAR_BUDGET && sections.length > 1) {
|
|
351
|
-
sections.pop();
|
|
352
|
-
combined = sections.join('\n\n');
|
|
353
|
-
}
|
|
354
|
-
if (combined.length > PROMPT_INJECTION_CHAR_BUDGET) {
|
|
355
|
-
combined = excerpt(combined, PROMPT_INJECTION_CHAR_BUDGET);
|
|
356
|
-
}
|
|
357
|
-
return combined;
|
|
358
|
-
}
|
package/unify/memory/schema.js
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* schema.js — task-334f R6 Memory schema constants.
|
|
3
|
-
*
|
|
4
|
-
* References:
|
|
5
|
-
* §Δ22 Memory 本质定性 (6 公理)
|
|
6
|
-
* §Δ23 Memory Entry Schema 扩字段
|
|
7
|
-
* §Δ25 Shard 语义初始分类
|
|
8
|
-
* §Δ26.3 Shard 软上限
|
|
9
|
-
*
|
|
10
|
-
* This module is **data only** — no I/O, no LLM calls. It is safe to import
|
|
11
|
-
* from any layer (store, recall, tools) without circular hazards.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
// ─── §Δ25.1 VP-memory default shard set ─────────────────────────
|
|
15
|
-
export const VP_DEFAULT_SHARDS = Object.freeze([
|
|
16
|
-
'skill',
|
|
17
|
-
'relations',
|
|
18
|
-
'lessons',
|
|
19
|
-
'preferences',
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
|
-
// ─── §Δ25.2 Feature-memory fixed 5 shards ───────────────────────
|
|
23
|
-
export const FEATURE_SHARDS = Object.freeze([
|
|
24
|
-
'decision',
|
|
25
|
-
'progress',
|
|
26
|
-
'context',
|
|
27
|
-
'blocker',
|
|
28
|
-
'artifact',
|
|
29
|
-
]);
|
|
30
|
-
|
|
31
|
-
// ─── §Δ25.3 User-memory default shard set ───────────────────────
|
|
32
|
-
export const USER_SHARDS = Object.freeze([
|
|
33
|
-
'profile',
|
|
34
|
-
'preferences',
|
|
35
|
-
'projects',
|
|
36
|
-
'goals',
|
|
37
|
-
'relations',
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* §Δ26.3 soft-cap table. `project-<slug>` is matched via the dedicated
|
|
42
|
-
* helper `softCapFor()` below because its key is dynamic.
|
|
43
|
-
*
|
|
44
|
-
* Shape: { entries: number, bytes: number }
|
|
45
|
-
*/
|
|
46
|
-
export const SOFT_CAPS = Object.freeze({
|
|
47
|
-
// VP
|
|
48
|
-
skill: { entries: 80, bytes: 64 * 1024 },
|
|
49
|
-
lessons: { entries: 80, bytes: 64 * 1024 },
|
|
50
|
-
preferences: { entries: 80, bytes: 64 * 1024 },
|
|
51
|
-
relations: { entries: 50, bytes: 32 * 1024 },
|
|
52
|
-
// Feature (Δ26.3 feature-memory row)
|
|
53
|
-
decision: { entries: 40, bytes: 24 * 1024 },
|
|
54
|
-
progress: { entries: 40, bytes: 24 * 1024 },
|
|
55
|
-
context: { entries: 40, bytes: 24 * 1024 },
|
|
56
|
-
blocker: { entries: 40, bytes: 24 * 1024 },
|
|
57
|
-
artifact: { entries: 40, bytes: 24 * 1024 },
|
|
58
|
-
// User (Δ26.3 user-memory row — 60 entries / 48 KiB per shard)
|
|
59
|
-
profile: { entries: 60, bytes: 48 * 1024 },
|
|
60
|
-
projects: { entries: 60, bytes: 48 * 1024 },
|
|
61
|
-
goals: { entries: 60, bytes: 48 * 1024 },
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
/** Project shards are dynamic: `project-<slug>` → 150 entries / 128 KiB. */
|
|
65
|
-
export const PROJECT_SHARD_SOFT_CAP = Object.freeze({
|
|
66
|
-
entries: 150,
|
|
67
|
-
bytes: 128 * 1024,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
/** Default cap if callers ask for a shard not in the canonical set. */
|
|
71
|
-
export const DEFAULT_SOFT_CAP = Object.freeze({
|
|
72
|
-
entries: 80,
|
|
73
|
-
bytes: 64 * 1024,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Trigger threshold: when a dream sweep finds ≥ PROJECT_DERIVE_THRESHOLD
|
|
78
|
-
* memory entries tagged with the same groupId, it may derive a
|
|
79
|
-
* `project-<slug>` shard. (§Δ25.1)
|
|
80
|
-
*/
|
|
81
|
-
export const PROJECT_DERIVE_THRESHOLD = 30;
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Max number of VP-memory shards — re-compression kicks in when exceeded
|
|
85
|
-
* (§Δ25.1: "最大 shard 数软上限 12").
|
|
86
|
-
*/
|
|
87
|
-
export const MAX_VP_SHARDS = 12;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Return the soft cap for a given shard name.
|
|
91
|
-
* Handles the dynamic `project-<slug>` case.
|
|
92
|
-
*/
|
|
93
|
-
export function softCapFor(shardName) {
|
|
94
|
-
if (typeof shardName !== 'string' || !shardName) return DEFAULT_SOFT_CAP;
|
|
95
|
-
if (shardName.startsWith('project-')) return PROJECT_SHARD_SOFT_CAP;
|
|
96
|
-
return SOFT_CAPS[shardName] || DEFAULT_SOFT_CAP;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Build a schema object suitable for `openShardStore(dir, schema)` (334o).
|
|
101
|
-
*
|
|
102
|
-
* @param {'vp'|'feature'|'user'} kind
|
|
103
|
-
* @param {{ extraShards?: string[] }} [opts] e.g. existing project shards
|
|
104
|
-
* @returns {{ shards: string[], softCap: Record<string,{entries:number,bytes:number}>, defaultSoftCap: object }}
|
|
105
|
-
*/
|
|
106
|
-
export function buildShardSchema(kind, opts = {}) {
|
|
107
|
-
let shards;
|
|
108
|
-
switch (kind) {
|
|
109
|
-
case 'vp': shards = [...VP_DEFAULT_SHARDS]; break;
|
|
110
|
-
case 'feature': shards = [...FEATURE_SHARDS]; break;
|
|
111
|
-
case 'user': shards = [...USER_SHARDS]; break;
|
|
112
|
-
default: throw new Error(`buildShardSchema: unknown kind "${kind}"`);
|
|
113
|
-
}
|
|
114
|
-
if (Array.isArray(opts.extraShards)) {
|
|
115
|
-
for (const s of opts.extraShards) {
|
|
116
|
-
if (typeof s === 'string' && s && !shards.includes(s)) shards.push(s);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
const softCap = {};
|
|
120
|
-
for (const s of shards) softCap[s] = softCapFor(s);
|
|
121
|
-
return {
|
|
122
|
-
shards,
|
|
123
|
-
softCap,
|
|
124
|
-
defaultSoftCap: DEFAULT_SOFT_CAP,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* R6 memory-entry authored-by enum (§Δ23).
|
|
130
|
-
* Free-form strings are allowed; these are canonical examples.
|
|
131
|
-
*/
|
|
132
|
-
export const AUTHORED_BY = Object.freeze({
|
|
133
|
-
VP: (vpId) => `vp:${vpId}`,
|
|
134
|
-
USER: (uid) => `user:${uid}`,
|
|
135
|
-
SUMMARY: 'system:summary-extractor',
|
|
136
|
-
DREAM: 'system:dream',
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Validate an R6 entry shape (schema-level, no I/O).
|
|
141
|
-
* Throws on structural violation. Callers that want soft warnings should
|
|
142
|
-
* wrap in try/catch.
|
|
143
|
-
*/
|
|
144
|
-
export function validateR6Entry(entry) {
|
|
145
|
-
if (!entry || typeof entry !== 'object') throw new Error('entry must be an object');
|
|
146
|
-
if (!entry.id || typeof entry.id !== 'string') throw new Error('entry.id required');
|
|
147
|
-
if (!entry.shard || typeof entry.shard !== 'string') throw new Error('entry.shard required');
|
|
148
|
-
if (!entry.kind || typeof entry.kind !== 'string') throw new Error('entry.kind required');
|
|
149
|
-
// sourceRef is required except for identity/preference pure-declaration entries.
|
|
150
|
-
const needsSourceRef = !(entry.kind === 'identity' || entry.kind === 'preference');
|
|
151
|
-
if (needsSourceRef) {
|
|
152
|
-
if (!entry.sourceRef || typeof entry.sourceRef !== 'object') {
|
|
153
|
-
throw new Error('entry.sourceRef required for kind=' + entry.kind);
|
|
154
|
-
}
|
|
155
|
-
if (!Array.isArray(entry.sourceRef.msgIds) || entry.sourceRef.msgIds.length === 0) {
|
|
156
|
-
throw new Error('entry.sourceRef.msgIds required (non-empty array)');
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (entry.supersedes != null && !Array.isArray(entry.supersedes)) {
|
|
160
|
-
throw new Error('entry.supersedes must be an array when present');
|
|
161
|
-
}
|
|
162
|
-
if (entry.supersededBy != null && typeof entry.supersededBy !== 'string') {
|
|
163
|
-
throw new Error('entry.supersededBy must be a string when present');
|
|
164
|
-
}
|
|
165
|
-
return true;
|
|
166
|
-
}
|