@yeaft/webchat-agent 0.1.530 → 0.1.532
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 +17 -1
- package/package.json +1 -1
- package/unify/engine.js +3 -0
- package/unify/memory/migrate-r5-to-r6.js +61 -0
- package/unify/memory/recall-r6.js +291 -0
- package/unify/memory/schema.js +166 -0
- package/unify/memory/shard-store.js +373 -0
- package/unify/task-message.js +144 -0
- package/unify/tools/index.js +4 -0
- package/unify/tools/memory-trace.js +135 -0
- package/unify/tools/open-source-message.js +49 -0
- package/unify/user-memory.js +104 -0
- package/unify/web-bridge.js +39 -0
|
@@ -36,7 +36,7 @@ import { sendToServer, flushMessageBuffer } from './buffer.js';
|
|
|
36
36
|
import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
|
|
37
37
|
import { loadMcpServers, updateMcpConfig } from '../mcp.js';
|
|
38
38
|
import { getLlmConfig, updateLlmConfig, getUnifySettings, updateUnifySettings } from '../unify/config-api.js';
|
|
39
|
-
import { handleUnifyChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyMergeThread, handleUnifyForkThread, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead } from '../unify/web-bridge.js';
|
|
39
|
+
import { handleUnifyChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyMergeThread, handleUnifyForkThread, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyTaskMessage, handleUnifyUserMemoryWrite, handleUnifyUserMemoryRemove } from '../unify/web-bridge.js';
|
|
40
40
|
|
|
41
41
|
export async function handleMessage(msg) {
|
|
42
42
|
switch (msg.type) {
|
|
@@ -420,6 +420,22 @@ export async function handleMessage(msg) {
|
|
|
420
420
|
handleUnifyVpRead(msg);
|
|
421
421
|
break;
|
|
422
422
|
|
|
423
|
+
// task-334h (R6 §Δ28 / §Δ31.6): task-scoped direct message echo.
|
|
424
|
+
// Replaces the withdrawn R3 `unify_task_private_chat`. Agent validates,
|
|
425
|
+
// stamps msgId + ts, and broadcasts the `task_message` mirror back.
|
|
426
|
+
case 'unify_task_message':
|
|
427
|
+
handleUnifyTaskMessage(msg);
|
|
428
|
+
break;
|
|
429
|
+
|
|
430
|
+
// task-334h (R6 §Δ29): user-memory skeleton. Payload schema + event
|
|
431
|
+
// names are wire-frozen here; real ingestion lands in task-334l.
|
|
432
|
+
case 'unify_user_memory_write':
|
|
433
|
+
handleUnifyUserMemoryWrite(msg);
|
|
434
|
+
break;
|
|
435
|
+
case 'unify_user_memory_remove':
|
|
436
|
+
handleUnifyUserMemoryRemove(msg);
|
|
437
|
+
break;
|
|
438
|
+
|
|
423
439
|
// Expert roles definition (for ExpertPanel detail view)
|
|
424
440
|
case 'get_expert_roles': {
|
|
425
441
|
const { getExpertRolesDefinition } = await import('../expert-roles.js');
|
package/package.json
CHANGED
package/unify/engine.js
CHANGED
|
@@ -310,6 +310,9 @@ export class Engine {
|
|
|
310
310
|
memoryInjection,
|
|
311
311
|
compactSummary,
|
|
312
312
|
skillContent,
|
|
313
|
+
// task-334f: memory_trace tool is now registered (49 → 51 tools), so
|
|
314
|
+
// unlock the core_memory meta-line behind 334e's feature flag.
|
|
315
|
+
memoryTraceAvailable: true,
|
|
313
316
|
});
|
|
314
317
|
}
|
|
315
318
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migrate-r5-to-r6.js — task-334f §Δ23 migration stub.
|
|
3
|
+
*
|
|
4
|
+
* Legacy (R5): `~/.yeaft/memory/entries/<slug>.md` plus numeric shard files
|
|
5
|
+
* `memory-001.md`, `memory-002.md`, ...
|
|
6
|
+
*
|
|
7
|
+
* R6: `~/.yeaft/memory/vp/<vpId>/memory-<semantic>.md`
|
|
8
|
+
* Semantic shards: skill / relations / lessons / preferences /
|
|
9
|
+
* project-<slug>
|
|
10
|
+
*
|
|
11
|
+
* This slice (334f) only DEFINES the API surface and a dry-run classifier.
|
|
12
|
+
* The actual batch migration runs in 334i; 334f does not mutate disk.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
import { parseEntry } from './store.js';
|
|
18
|
+
import { classifyLegacyEntryToShard } from './shard-store.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Produce a migration plan without applying it.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} legacyEntriesDir e.g. ~/.yeaft/memory/entries
|
|
24
|
+
* @returns {{
|
|
25
|
+
* totalEntries: number,
|
|
26
|
+
* plan: Array<{ slug: string, shard: string, kind: string, tags: string[] }>,
|
|
27
|
+
* byShard: Record<string, number>
|
|
28
|
+
* }}
|
|
29
|
+
*/
|
|
30
|
+
export function planR5ToR6Migration(legacyEntriesDir) {
|
|
31
|
+
if (!legacyEntriesDir || !existsSync(legacyEntriesDir)) {
|
|
32
|
+
return { totalEntries: 0, plan: [], byShard: {} };
|
|
33
|
+
}
|
|
34
|
+
const files = readdirSync(legacyEntriesDir).filter(f => f.endsWith('.md'));
|
|
35
|
+
const plan = [];
|
|
36
|
+
const byShard = {};
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
const raw = readFileSync(join(legacyEntriesDir, file), 'utf8');
|
|
39
|
+
const entry = parseEntry(raw);
|
|
40
|
+
if (!entry) continue;
|
|
41
|
+
const shard = classifyLegacyEntryToShard(entry);
|
|
42
|
+
plan.push({
|
|
43
|
+
slug: file.replace(/\.md$/, ''),
|
|
44
|
+
shard,
|
|
45
|
+
kind: entry.kind || 'fact',
|
|
46
|
+
tags: entry.tags || [],
|
|
47
|
+
});
|
|
48
|
+
byShard[shard] = (byShard[shard] || 0) + 1;
|
|
49
|
+
}
|
|
50
|
+
return { totalEntries: plan.length, plan, byShard };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Apply the migration. STUB — 334i will fill in the body writer. 334f keeps
|
|
55
|
+
* this function exported so downstream tests can assert the hook exists.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} _opts { legacyEntriesDir, targetDir, vpId, dryRun }
|
|
58
|
+
*/
|
|
59
|
+
export async function applyR5ToR6Migration(_opts) {
|
|
60
|
+
throw new Error('applyR5ToR6Migration: not yet implemented (task-334i)');
|
|
61
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recall-r6.js — task-334f R6 4-step recall pipeline (§Δ24.2).
|
|
3
|
+
*
|
|
4
|
+
* Pipeline:
|
|
5
|
+
* Step 1: Shard classifier — fastModel OR keyword heuristic → top-1~2 shards
|
|
6
|
+
* Step 2: In-shard candidate — scan only the selected shards, filter by kind/tags/pinned
|
|
7
|
+
* Step 3: LLM rerank — pick top-7 from candidates
|
|
8
|
+
* Step 4: Inject body only — return entries without sourceRef (§Δ23)
|
|
9
|
+
*
|
|
10
|
+
* The legacy R5 recall (agent/unify/memory/recall.js) remains for back-compat
|
|
11
|
+
* of callers still on MemoryStore. R6 callers should use `recallR6()` below.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from 'crypto';
|
|
15
|
+
import { pickEffort } from '../effort.js';
|
|
16
|
+
import {
|
|
17
|
+
VP_DEFAULT_SHARDS,
|
|
18
|
+
TASK_SHARDS,
|
|
19
|
+
USER_SHARDS,
|
|
20
|
+
} from './schema.js';
|
|
21
|
+
|
|
22
|
+
const MAX_RECALL_RESULTS = 7;
|
|
23
|
+
const MAX_CANDIDATES = 15;
|
|
24
|
+
|
|
25
|
+
// ─── Step 1: Shard Classifier ───────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Keyword → shard heuristic. Zero-cost fallback when fastModel is unavailable.
|
|
29
|
+
*
|
|
30
|
+
* The lexicons are intentionally small: classifier output only needs to point
|
|
31
|
+
* at the most likely shard; LLM rerank in Step 3 catches misses. Empirically
|
|
32
|
+
* this covers > 70% of queries with zero LLM cost.
|
|
33
|
+
*/
|
|
34
|
+
const SHARD_LEXICON = {
|
|
35
|
+
skill: ['code', 'api', 'library', 'framework', 'implement', 'debug', 'syntax', 'typescript', 'vue', 'pattern', '代码', '实现', '调试', '语法', '模式', '技术'],
|
|
36
|
+
lessons: ['mistake', 'avoid', 'lesson', 'pitfall', 'gotcha', 'bug', 'regression', '教训', '避坑', '坑', '踩坑', '反模式'],
|
|
37
|
+
preferences: ['prefer', 'like', 'style', 'convention', 'favorite', '偏好', '风格', '习惯', '喜欢'],
|
|
38
|
+
relations: ['colleague', 'partner', 'team', 'user', 'collaborator', 'vp', '同事', '队友', '协作', '关系'],
|
|
39
|
+
// Task-memory shards
|
|
40
|
+
decision: ['decide', 'decision', 'chose', 'picked', 'resolved', '决定', '决策', '选择'],
|
|
41
|
+
progress: ['done', 'progress', 'milestone', 'shipped', 'finished', '完成', '进度', '交付'],
|
|
42
|
+
context: ['background', 'context', 'requirement', 'scope', '背景', '需求', '范围'],
|
|
43
|
+
blocker: ['block', 'stuck', 'blocker', 'issue', 'waiting', '阻塞', '卡住', '等待'],
|
|
44
|
+
artifact: ['pr', 'commit', 'doc', 'file', 'link', 'artifact', 'deliverable', 'commit', '产出', '文档'],
|
|
45
|
+
// User-memory shards
|
|
46
|
+
profile: ['name', 'role', 'background', 'who', 'identity', '身份', '角色', '背景'],
|
|
47
|
+
projects: ['project', 'repo', '项目', '仓库'],
|
|
48
|
+
goals: ['goal', 'target', 'okr', 'plan', '目标', '计划'],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Pick top-N shards via keyword heuristic.
|
|
53
|
+
* @param {string} prompt
|
|
54
|
+
* @param {string[]} availableShards all shards present in this store
|
|
55
|
+
* @param {number} [topN=2]
|
|
56
|
+
*/
|
|
57
|
+
export function classifyShardsByKeyword(prompt, availableShards, topN = 2) {
|
|
58
|
+
if (!prompt || !availableShards || availableShards.length === 0) return [];
|
|
59
|
+
const lower = prompt.toLowerCase();
|
|
60
|
+
const scores = new Map();
|
|
61
|
+
for (const shard of availableShards) {
|
|
62
|
+
// project-<slug> shards score via their slug
|
|
63
|
+
const lex = SHARD_LEXICON[shard] || [];
|
|
64
|
+
let score = 0;
|
|
65
|
+
for (const kw of lex) {
|
|
66
|
+
if (lower.includes(kw)) score += 1;
|
|
67
|
+
}
|
|
68
|
+
if (shard.startsWith('project-')) {
|
|
69
|
+
const slug = shard.slice('project-'.length);
|
|
70
|
+
if (slug && lower.includes(slug.toLowerCase())) score += 3;
|
|
71
|
+
}
|
|
72
|
+
if (score > 0) scores.set(shard, score);
|
|
73
|
+
}
|
|
74
|
+
const ranked = [...scores.entries()].sort((a, b) => b[1] - a[1]).map(([s]) => s);
|
|
75
|
+
if (ranked.length >= 1) return ranked.slice(0, topN);
|
|
76
|
+
// Fallback: first N defaults in the shard set
|
|
77
|
+
return availableShards.slice(0, topN);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Shard classifier — calls fastModel if adapter provided, else falls back to
|
|
82
|
+
* the keyword heuristic. Budget < 200 tokens (§Δ24.2).
|
|
83
|
+
*/
|
|
84
|
+
export async function classifyShards({
|
|
85
|
+
prompt,
|
|
86
|
+
availableShards,
|
|
87
|
+
adapter,
|
|
88
|
+
fastModel,
|
|
89
|
+
topN = 2,
|
|
90
|
+
}) {
|
|
91
|
+
if (!adapter || !fastModel) {
|
|
92
|
+
return classifyShardsByKeyword(prompt, availableShards, topN);
|
|
93
|
+
}
|
|
94
|
+
const system = `You classify user queries into memory shards. Return ONLY a JSON array of up to ${topN} shard names from the provided list. No prose.`;
|
|
95
|
+
const user = `Available shards: ${JSON.stringify(availableShards)}
|
|
96
|
+
Query: ${JSON.stringify(prompt)}
|
|
97
|
+
Return JSON array of up to ${topN} most relevant shard names.`;
|
|
98
|
+
try {
|
|
99
|
+
const res = await adapter.call({
|
|
100
|
+
model: fastModel,
|
|
101
|
+
system,
|
|
102
|
+
messages: [{ role: 'user', content: user }],
|
|
103
|
+
maxTokens: 64,
|
|
104
|
+
effort: pickEffort({ scenario: 'recall' }),
|
|
105
|
+
});
|
|
106
|
+
const m = (res.text || '').match(/\[[\s\S]*?\]/);
|
|
107
|
+
if (!m) return classifyShardsByKeyword(prompt, availableShards, topN);
|
|
108
|
+
const arr = JSON.parse(m[0]);
|
|
109
|
+
const valid = arr.filter(s => typeof s === 'string' && availableShards.includes(s));
|
|
110
|
+
if (valid.length === 0) return classifyShardsByKeyword(prompt, availableShards, topN);
|
|
111
|
+
return valid.slice(0, topN);
|
|
112
|
+
} catch {
|
|
113
|
+
return classifyShardsByKeyword(prompt, availableShards, topN);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Step 2: In-shard candidate generation ──────────────────────
|
|
118
|
+
|
|
119
|
+
function collectCandidates(memoryShardStore, { shards, kind, tags, pinned }) {
|
|
120
|
+
const filter = {};
|
|
121
|
+
if (shards && shards.length) filter.shard = shards.length === 1 ? shards[0] : shards;
|
|
122
|
+
if (kind) filter.kind = kind;
|
|
123
|
+
if (tags && tags.length) filter.tags = tags;
|
|
124
|
+
if (pinned !== undefined) filter.pinned = pinned;
|
|
125
|
+
const { results } = memoryShardStore.query(filter);
|
|
126
|
+
return results
|
|
127
|
+
// Drop superseded entries from the candidate pool (they stay on disk
|
|
128
|
+
// for memory_trace but should not compete for recall slots).
|
|
129
|
+
.filter(rec => !rec.supersededBy)
|
|
130
|
+
.slice(0, MAX_CANDIDATES);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Step 3: LLM Rerank ─────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
async function llmRerank({ adapter, fastModel, prompt, candidates, memoryShardStore }) {
|
|
136
|
+
if (candidates.length <= MAX_RECALL_RESULTS) {
|
|
137
|
+
return candidates.map(c => c.id);
|
|
138
|
+
}
|
|
139
|
+
const lines = candidates.map((c, i) => {
|
|
140
|
+
return `${i + 1}. [id=${c.id}] shard=${c.shard} kind=${c.kind || '?'} tags=[${(c.tags || []).join(',')}]`;
|
|
141
|
+
}).join('\n');
|
|
142
|
+
const system = `You pick the most relevant memories for the user's prompt. Return ONLY a JSON array of entry ids (up to ${MAX_RECALL_RESULTS}). No prose.`;
|
|
143
|
+
const user = `User prompt: ${JSON.stringify(prompt)}
|
|
144
|
+
|
|
145
|
+
Candidate memories:
|
|
146
|
+
${lines}
|
|
147
|
+
|
|
148
|
+
Return JSON array of up to ${MAX_RECALL_RESULTS} ids.`;
|
|
149
|
+
try {
|
|
150
|
+
const res = await adapter.call({
|
|
151
|
+
model: fastModel,
|
|
152
|
+
system,
|
|
153
|
+
messages: [{ role: 'user', content: user }],
|
|
154
|
+
maxTokens: 256,
|
|
155
|
+
effort: pickEffort({ scenario: 'recall' }),
|
|
156
|
+
});
|
|
157
|
+
const m = (res.text || '').match(/\[[\s\S]*\]/);
|
|
158
|
+
if (!m) return candidates.slice(0, MAX_RECALL_RESULTS).map(c => c.id);
|
|
159
|
+
const arr = JSON.parse(m[0]).filter(x => typeof x === 'string');
|
|
160
|
+
const valid = arr.filter(id => candidates.find(c => c.id === id));
|
|
161
|
+
if (valid.length === 0) return candidates.slice(0, MAX_RECALL_RESULTS).map(c => c.id);
|
|
162
|
+
return valid.slice(0, MAX_RECALL_RESULTS);
|
|
163
|
+
} catch {
|
|
164
|
+
return candidates.slice(0, MAX_RECALL_RESULTS).map(c => c.id);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Step 4: Inject (no sourceRef) ──────────────────────────────
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Produce the body-only injection payload (§Δ24.5). Prefix lines with
|
|
172
|
+
* `[mem:<shard>]` so the LLM knows the category without seeing the id.
|
|
173
|
+
*/
|
|
174
|
+
export function formatForInjection(entries) {
|
|
175
|
+
return entries.map(e => {
|
|
176
|
+
const prefix = `[mem:${e.shard}]`;
|
|
177
|
+
const body = (e.body || '').trim();
|
|
178
|
+
return `${prefix} ${body}`;
|
|
179
|
+
}).join('\n\n');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── Fingerprint cache ──────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
const _cache = new Map();
|
|
185
|
+
const CACHE_TTL = 5 * 60 * 1000;
|
|
186
|
+
|
|
187
|
+
function computeFingerprint({ shards, prompt, taskId }) {
|
|
188
|
+
const head = prompt.slice(0, 200);
|
|
189
|
+
const input = `${shards.join(',')}|${head}|${taskId || ''}`;
|
|
190
|
+
return createHash('sha256').update(input).digest('hex').slice(0, 16);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ─── Entry point ────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Run the R6 4-step recall pipeline.
|
|
197
|
+
*
|
|
198
|
+
* @param {{
|
|
199
|
+
* prompt: string,
|
|
200
|
+
* memoryShardStore: object, // openMemoryShardStore() handle
|
|
201
|
+
* adapter?: object, // LLM adapter (null = keyword-only)
|
|
202
|
+
* fastModel?: string, // fast model for classifier + rerank
|
|
203
|
+
* availableShards?: string[], // defaults to store stats
|
|
204
|
+
* taskId?: string,
|
|
205
|
+
* kind?: string, tags?: string[], pinned?: boolean,
|
|
206
|
+
* }} params
|
|
207
|
+
* @returns {Promise<{entries: object[], shards: string[], fingerprint: string, cached: boolean}>}
|
|
208
|
+
*/
|
|
209
|
+
export async function recallR6(params) {
|
|
210
|
+
const {
|
|
211
|
+
prompt,
|
|
212
|
+
memoryShardStore,
|
|
213
|
+
adapter,
|
|
214
|
+
fastModel,
|
|
215
|
+
availableShards,
|
|
216
|
+
taskId,
|
|
217
|
+
kind,
|
|
218
|
+
tags,
|
|
219
|
+
pinned,
|
|
220
|
+
} = params;
|
|
221
|
+
|
|
222
|
+
if (!prompt || !prompt.trim() || !memoryShardStore) {
|
|
223
|
+
return { entries: [], shards: [], fingerprint: '', cached: false };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const shardsFromStore = availableShards
|
|
227
|
+
|| Object.keys(memoryShardStore.stats().shards);
|
|
228
|
+
|
|
229
|
+
if (shardsFromStore.length === 0) {
|
|
230
|
+
return { entries: [], shards: [], fingerprint: '', cached: false };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Step 1
|
|
234
|
+
const chosenShards = await classifyShards({
|
|
235
|
+
prompt,
|
|
236
|
+
availableShards: shardsFromStore,
|
|
237
|
+
adapter,
|
|
238
|
+
fastModel,
|
|
239
|
+
topN: 2,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const fingerprint = computeFingerprint({ shards: chosenShards, prompt, taskId });
|
|
243
|
+
const cached = _cache.get(fingerprint);
|
|
244
|
+
if (cached && Date.now() - cached.t < CACHE_TTL) {
|
|
245
|
+
return { entries: cached.entries, shards: chosenShards, fingerprint, cached: true };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Step 2
|
|
249
|
+
const candidates = collectCandidates(memoryShardStore, {
|
|
250
|
+
shards: chosenShards,
|
|
251
|
+
kind,
|
|
252
|
+
tags,
|
|
253
|
+
pinned,
|
|
254
|
+
});
|
|
255
|
+
if (candidates.length === 0) {
|
|
256
|
+
_cache.set(fingerprint, { entries: [], t: Date.now() });
|
|
257
|
+
return { entries: [], shards: chosenShards, fingerprint, cached: false };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Step 3
|
|
261
|
+
const selectedIds = await llmRerank({
|
|
262
|
+
adapter, fastModel, prompt, candidates, memoryShardStore,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Step 4 — load full bodies (minus sourceRef for injection).
|
|
266
|
+
const entries = [];
|
|
267
|
+
for (const id of selectedIds) {
|
|
268
|
+
const full = memoryShardStore.get(id);
|
|
269
|
+
if (!full) continue;
|
|
270
|
+
entries.push({
|
|
271
|
+
id: full.id,
|
|
272
|
+
shard: full.shard,
|
|
273
|
+
kind: full.kind,
|
|
274
|
+
body: full.body,
|
|
275
|
+
tags: full.tags || [],
|
|
276
|
+
// deliberately do NOT expose sourceRef here (§Δ23)
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
_cache.set(fingerprint, { entries, t: Date.now() });
|
|
280
|
+
return { entries, shards: chosenShards, fingerprint, cached: false };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function clearR6RecallCache() {
|
|
284
|
+
_cache.clear();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export const R6_DEFAULTS = {
|
|
288
|
+
VP_DEFAULT_SHARDS,
|
|
289
|
+
TASK_SHARDS,
|
|
290
|
+
USER_SHARDS,
|
|
291
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
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 Task-memory fixed 5 shards ──────────────────────────
|
|
23
|
+
export const TASK_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
|
+
// Task (Δ26.3 task-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'|'task'|'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 'task': shards = [...TASK_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
|
+
}
|