@yeaft/webchat-agent 0.1.965 → 0.1.966
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/package.json +1 -1
- package/yeaft/dream/runner.js +48 -10
- package/yeaft/dream/segment-extract.js +239 -0
- package/yeaft/dream/session-wiring.js +19 -15
- package/yeaft/engine.js +38 -6
- package/yeaft/memory/segment.js +1 -1
- package/yeaft/prompts.js +26 -15
package/package.json
CHANGED
package/yeaft/dream/runner.js
CHANGED
|
@@ -10,17 +10,17 @@
|
|
|
10
10
|
* for each session with newCount ≥ MIN_NEW_PER_GROUP (auto)
|
|
11
11
|
* or > 0 (manual)
|
|
12
12
|
* or prior messages in a scoped manual session rerun:
|
|
13
|
-
* loadDiff() via opts.
|
|
13
|
+
* loadDiff() via opts.loadSessionDiff(sessionId, sinceId)
|
|
14
14
|
* applyOverlap() via opts.loadOverlapPreamble(...)
|
|
15
15
|
* segment() segmentDiff(...)
|
|
16
|
-
*
|
|
16
|
+
* triage segments → session-local actions[]
|
|
17
17
|
*
|
|
18
18
|
* mergeByTarget() → per-target actions
|
|
19
19
|
* for each merged target:
|
|
20
20
|
* applyMergedTarget() (snapshot + UPDATE/CREATE + atomic write)
|
|
21
21
|
*
|
|
22
22
|
* bookkeep:
|
|
23
|
-
* for each processed
|
|
23
|
+
* for each processed session:
|
|
24
24
|
* session .dream-state ←
|
|
25
25
|
* { lastDreamMessageId: tail of real diff,
|
|
26
26
|
* lastDreamAt: nowIso,
|
|
@@ -50,6 +50,7 @@ import { segmentDiff, truncateMessage, estimateMessagesTokens } from './segment.
|
|
|
50
50
|
import { triageGroupSegments } from './triage.js';
|
|
51
51
|
import { mergeByTarget } from './merge.js';
|
|
52
52
|
import { applyMergedTarget } from './apply.js';
|
|
53
|
+
import { extractAndWriteMemorySegments } from './segment-extract.js';
|
|
53
54
|
import { tsForBackup, pruneOldSnapshots } from './snapshot.js';
|
|
54
55
|
|
|
55
56
|
/**
|
|
@@ -58,12 +59,14 @@ import { tsForBackup, pruneOldSnapshots } from './snapshot.js';
|
|
|
58
59
|
* @property {boolean} [manual=false] — manual trigger overrides newCount<20 skip
|
|
59
60
|
* @property {string[]} [scopeFilter] — optional: only dream these targets; scoped manual session triggers rerun the current session when there are prior messages but no new cursor delta ('*' allowed)
|
|
60
61
|
* @property {(req: {pass:string, prompt:string, system:string}) => Promise<string>} llm
|
|
61
|
-
* @property {() => Promise<Array<string>>} listSessions — return all session ids (incl. '_no-
|
|
62
|
-
* @property {(sessionId: string) => Promise<number>} countMessages — total message count for a
|
|
63
|
-
* @property {(sessionId: string, sinceMessageId: string|null) => Promise<Array<object>>}
|
|
62
|
+
* @property {() => Promise<Array<string>>} listSessions — return all session ids (incl. '_no-session')
|
|
63
|
+
* @property {(sessionId: string) => Promise<number>} countMessages — total message count for a session
|
|
64
|
+
* @property {(sessionId: string, sinceMessageId: string|null) => Promise<Array<object>>} [loadSessionDiff]
|
|
65
|
+
* @property {(sessionId: string, sinceMessageId: string|null) => Promise<Array<object>>} [loadGroupDiff] — legacy alias for loadSessionDiff
|
|
64
66
|
* @property {(sessionId: string, beforeMessageId: string|null, count: number) => Promise<Array<object>>} loadOverlapPreamble
|
|
65
67
|
* @property {() => Promise<Array<{path:string, summary:string}>>} [listTopicSummaries]
|
|
66
68
|
* @property {(target: string) => Promise<Array<{path:string, summary:string}>>} [siblingTopicsFor]
|
|
69
|
+
* @property {import('../memory/index-db.js').SegmentIndex|null} [segmentIndex] — optional derived FTS segment index to sync after segment writes
|
|
67
70
|
* @property {(event: object) => void} [onProgress]
|
|
68
71
|
* @property {object} [limits] — override DEFAULT_LIMITS
|
|
69
72
|
* @property {() => string} [nowIso]
|
|
@@ -86,7 +89,7 @@ export async function runDream(opts) {
|
|
|
86
89
|
|
|
87
90
|
onProgress({ phase: 'start', manual: !!opts.manual, ts });
|
|
88
91
|
|
|
89
|
-
// 1. enumerate
|
|
92
|
+
// 1. enumerate sessions
|
|
90
93
|
const sessionIds = await safeCall(opts.listSessions, []);
|
|
91
94
|
const filter = Array.isArray(opts.scopeFilter) ? new Set(opts.scopeFilter) : null;
|
|
92
95
|
const sessionFilter = deriveSessionFilter(filter);
|
|
@@ -135,7 +138,8 @@ export async function runDream(opts) {
|
|
|
135
138
|
|
|
136
139
|
onProgress({ phase: 'load-diff', sessionId });
|
|
137
140
|
const diffCursor = rerunScopedManual ? null : state.lastDreamMessageId;
|
|
138
|
-
const
|
|
141
|
+
const loadDiff = opts.loadSessionDiff || opts.loadGroupDiff;
|
|
142
|
+
const diffNew = await safeCall(() => loadDiff(sessionId, diffCursor), []);
|
|
139
143
|
if (!diffNew || diffNew.length === 0) {
|
|
140
144
|
sessionsReport.push({ sessionId, new: newCount, status: 'skipped', reason: 'empty-diff' });
|
|
141
145
|
continue;
|
|
@@ -233,9 +237,42 @@ export async function runDream(opts) {
|
|
|
233
237
|
}
|
|
234
238
|
}
|
|
235
239
|
|
|
236
|
-
// 5.
|
|
240
|
+
// 5. extract atomic H2 memory segments. The apply step above keeps the
|
|
241
|
+
// coarse summary layer (`summary.md`); this step keeps bounded, evidence-
|
|
242
|
+
// backed current details in segment-formatted `memory.md`.
|
|
243
|
+
const segmentReports = [];
|
|
244
|
+
for (const triage of sessionTriages) {
|
|
245
|
+
const appliedTargets = new Set(targetsReport.filter(r => r.status === 'done').map(r => r.target));
|
|
246
|
+
const targets = triage.actions.map(a => a.scope).filter(scope => appliedTargets.has(scope));
|
|
247
|
+
if (targets.length === 0) continue;
|
|
248
|
+
try {
|
|
249
|
+
onProgress({ phase: 'extract-segments', sessionId: triage.sessionId, status: 'running', targets: targets.length });
|
|
250
|
+
const r = await extractAndWriteMemorySegments({
|
|
251
|
+
root: opts.root,
|
|
252
|
+
sessionId: triage.sessionId,
|
|
253
|
+
messages: triage.diff,
|
|
254
|
+
targets,
|
|
255
|
+
llm: opts.llm,
|
|
256
|
+
language: opts.language,
|
|
257
|
+
nowIso: opts.nowIso || (() => nowIso),
|
|
258
|
+
segmentIndex: opts.segmentIndex || null,
|
|
259
|
+
});
|
|
260
|
+
segmentReports.push({ sessionId: triage.sessionId, status: 'done', ...r });
|
|
261
|
+
onProgress({ phase: 'extract-segments', sessionId: triage.sessionId, status: 'done', ...r });
|
|
262
|
+
} catch (err) {
|
|
263
|
+
segmentReports.push({ sessionId: triage.sessionId, status: 'error', error: err.message });
|
|
264
|
+
onProgress({ phase: 'extract-segments', sessionId: triage.sessionId, status: 'error', error: err.message });
|
|
265
|
+
await writeDreamError(opts.root, `sessions/${triage.sessionId}`, {
|
|
266
|
+
phase: 'extract-segments',
|
|
267
|
+
message: err.message,
|
|
268
|
+
stack: err.stack,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 6. bookkeep — only when at least one apply for this session's actions
|
|
237
274
|
// succeeded. We use a permissive policy: if ANY merged-target apply
|
|
238
|
-
// succeeded for a
|
|
275
|
+
// succeeded for a session's contributed actions, advance that session's
|
|
239
276
|
// cursor. (If everything errored, we keep the cursor so next run
|
|
240
277
|
// retries.)
|
|
241
278
|
const successfulTargets = new Set(targetsReport.filter(r => r.status === 'done').map(r => r.target));
|
|
@@ -271,6 +308,7 @@ export async function runDream(opts) {
|
|
|
271
308
|
durationMs: duration,
|
|
272
309
|
sessions: sessionsReport,
|
|
273
310
|
targets: targetsReport,
|
|
311
|
+
memorySegments: segmentReports,
|
|
274
312
|
backups: pruned,
|
|
275
313
|
ts,
|
|
276
314
|
};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dream/segment-extract.js.
|
|
3
|
+
*
|
|
4
|
+
* Minimal H2 Dream bridge: extract atomic memory segments for the session
|
|
5
|
+
* scopes touched by a Dream pass and persist them through memory/segment-store.
|
|
6
|
+
* The existing apply path still maintains summary.md as the coarse layer.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readScope, writeScope } from '../memory/segment-store.js';
|
|
10
|
+
import { syncScope } from '../memory/segment-sync.js';
|
|
11
|
+
import { makeSegment } from '../memory/segment.js';
|
|
12
|
+
import { render, extractTemplateForScope } from './prompts/index.js';
|
|
13
|
+
import { parseJsonSafe } from './triage.js';
|
|
14
|
+
|
|
15
|
+
const MAX_TARGETS = 24;
|
|
16
|
+
const MAX_MESSAGES = 80;
|
|
17
|
+
const MAX_BODY_CHARS = 1200;
|
|
18
|
+
const MAX_SEGMENTS_PER_SCOPE = 64;
|
|
19
|
+
const RECENT_MESSAGE_COUNT = 8;
|
|
20
|
+
const VALID_KINDS = new Set(['fact', 'preference', 'decision', 'lesson', 'relation', 'goal', 'context']);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {{
|
|
24
|
+
* root: string,
|
|
25
|
+
* sessionId: string,
|
|
26
|
+
* messages: Array<object>,
|
|
27
|
+
* targets?: string[],
|
|
28
|
+
* llm: Function,
|
|
29
|
+
* language?: string,
|
|
30
|
+
* nowIso?: Function,
|
|
31
|
+
* segmentIndex?: import('../memory/index-db.js').SegmentIndex|null,
|
|
32
|
+
* }} opts
|
|
33
|
+
*/
|
|
34
|
+
export async function extractAndWriteMemorySegments(opts) {
|
|
35
|
+
if (!opts || !opts.root) throw new Error('extractAndWriteMemorySegments: root required');
|
|
36
|
+
if (!opts.sessionId) throw new Error('extractAndWriteMemorySegments: sessionId required');
|
|
37
|
+
if (typeof opts.llm !== 'function') throw new Error('extractAndWriteMemorySegments: llm required');
|
|
38
|
+
|
|
39
|
+
const messages = normalizeMessages(opts.messages || []);
|
|
40
|
+
if (messages.length === 0) return { scopes: 0, segments: 0, errors: [] };
|
|
41
|
+
|
|
42
|
+
const targetScopes = normalizeTargetScopes(opts.sessionId, opts.targets || []);
|
|
43
|
+
const now = opts.nowIso ? opts.nowIso() : new Date().toISOString();
|
|
44
|
+
let segmentCount = 0;
|
|
45
|
+
let scopeCount = 0;
|
|
46
|
+
const errors = [];
|
|
47
|
+
|
|
48
|
+
for (const scope of targetScopes.slice(0, MAX_TARGETS)) {
|
|
49
|
+
let extracted = [];
|
|
50
|
+
try {
|
|
51
|
+
extracted = await extractScopeSegments({
|
|
52
|
+
scope,
|
|
53
|
+
sessionId: opts.sessionId,
|
|
54
|
+
messages,
|
|
55
|
+
llm: opts.llm,
|
|
56
|
+
language: opts.language,
|
|
57
|
+
now,
|
|
58
|
+
});
|
|
59
|
+
} catch (err) {
|
|
60
|
+
errors.push({ scope, error: err.message, rawSnippet: err.rawSnippet || '' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const recent = scope === `sessions/${opts.sessionId}`
|
|
64
|
+
? [buildRecentSegment({ scope, messages, now })]
|
|
65
|
+
: [];
|
|
66
|
+
if (extracted.length === 0 && recent.length === 0) continue;
|
|
67
|
+
|
|
68
|
+
const nextSegments = mergeSegments(readScope(opts.root, scope), [...extracted, ...recent]);
|
|
69
|
+
writeScope(opts.root, scope, nextSegments);
|
|
70
|
+
if (opts.segmentIndex) syncScope(opts.root, opts.segmentIndex, scope);
|
|
71
|
+
segmentCount += extracted.length + recent.length;
|
|
72
|
+
scopeCount += 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { scopes: scopeCount, segments: segmentCount, errors };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function extractScopeSegments({ scope, sessionId, messages, llm, language, now }) {
|
|
79
|
+
const template = extractTemplateForScope(scope);
|
|
80
|
+
const base = render(template, templateVarsForScope(scope, sessionId), { language });
|
|
81
|
+
const prompt = `${base}\n\nTarget scope: ${scope}\n\nConversation diff, oldest first:\n${renderMessages(messages)}\n\nReturn only the JSON array. Do not wrap it in Markdown.`;
|
|
82
|
+
const firstRaw = await llm({ pass: 'extract-segments', prompt, system: extractSystem(language) });
|
|
83
|
+
const firstParsed = parseJsonSafe(firstRaw);
|
|
84
|
+
if (Array.isArray(firstParsed)) {
|
|
85
|
+
return firstParsed
|
|
86
|
+
.map(item => normalizeExtractedSegment({ item, scope, now }))
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const retryPrompt = `${prompt}\n\nYour previous output was malformed JSON. Previous output snippet:\n${rawSnippet(firstRaw)}\n\nRetry now. Return only a strict JSON array.`;
|
|
91
|
+
const retryRaw = await llm({ pass: 'extract-segments-retry', prompt: retryPrompt, system: extractSystem(language) });
|
|
92
|
+
const retryParsed = parseJsonSafe(retryRaw);
|
|
93
|
+
if (!Array.isArray(retryParsed)) {
|
|
94
|
+
const err = new Error(`extract-segments: malformed JSON for ${scope}`);
|
|
95
|
+
err.rawSnippet = rawSnippet(retryRaw || firstRaw);
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return retryParsed
|
|
100
|
+
.map(item => normalizeExtractedSegment({ item, scope, now }))
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function templateVarsForScope(scope, sessionId) {
|
|
105
|
+
const vars = { sessionId, vpId: '', topicId: '' };
|
|
106
|
+
const vpMatch = /^sessions\/[^/]+\/vp\/(.+)$/.exec(scope);
|
|
107
|
+
if (vpMatch) vars.vpId = vpMatch[1];
|
|
108
|
+
const topicMatch = /^sessions\/[^/]+\/topic\/(.+)$/.exec(scope);
|
|
109
|
+
if (topicMatch) vars.topicId = topicMatch[1];
|
|
110
|
+
return vars;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizeTargetScopes(sessionId, targets) {
|
|
114
|
+
const out = new Set(['user', `sessions/${sessionId}`, `sessions/${sessionId}/user`]);
|
|
115
|
+
for (const target of targets) {
|
|
116
|
+
if (typeof target !== 'string') continue;
|
|
117
|
+
const clean = target.trim();
|
|
118
|
+
if (!clean) continue;
|
|
119
|
+
if (clean === 'user' || clean.startsWith(`sessions/${sessionId}/`) || clean === `sessions/${sessionId}`) {
|
|
120
|
+
out.add(clean);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return [...out];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeMessages(messages) {
|
|
127
|
+
return messages
|
|
128
|
+
.filter(m => m && typeof m === 'object')
|
|
129
|
+
.slice(-MAX_MESSAGES)
|
|
130
|
+
.map((m, index) => ({
|
|
131
|
+
id: String(m.id || m.messageId || `dream_msg_${index}`),
|
|
132
|
+
role: String(m.role || m.type || 'unknown'),
|
|
133
|
+
vpId: typeof m.vpId === 'string' ? m.vpId : '',
|
|
134
|
+
body: String(m.body || m.content || '').slice(0, MAX_BODY_CHARS),
|
|
135
|
+
kind: String(m.kind || ''),
|
|
136
|
+
}))
|
|
137
|
+
.filter(m => m.body.trim());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderMessages(messages) {
|
|
141
|
+
return JSON.stringify(messages.map(m => ({
|
|
142
|
+
id: m.id,
|
|
143
|
+
role: m.role,
|
|
144
|
+
vpId: m.vpId || undefined,
|
|
145
|
+
kind: m.kind || undefined,
|
|
146
|
+
body: m.body,
|
|
147
|
+
})), null, 2);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeExtractedSegment({ item, scope, now }) {
|
|
151
|
+
if (!item || typeof item !== 'object') return null;
|
|
152
|
+
const body = String(item.body || item.content || item.summary || '').trim();
|
|
153
|
+
if (!body) return null;
|
|
154
|
+
const kind = VALID_KINDS.has(String(item.kind || '')) ? String(item.kind) : 'context';
|
|
155
|
+
const tags = Array.isArray(item.tags) ? item.tags.map(t => String(t).trim()).filter(Boolean) : [];
|
|
156
|
+
const sourceMessages = Array.isArray(item.sourceMessages)
|
|
157
|
+
? item.sourceMessages.map(id => String(id).trim()).filter(Boolean)
|
|
158
|
+
: [];
|
|
159
|
+
return makeSegment({
|
|
160
|
+
scope,
|
|
161
|
+
kind,
|
|
162
|
+
tags: [...new Set(tags)],
|
|
163
|
+
sourceMessages: [...new Set(sourceMessages)],
|
|
164
|
+
createdAt: typeof item.createdAt === 'string' ? item.createdAt : now,
|
|
165
|
+
updatedAt: now,
|
|
166
|
+
body,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildRecentSegment({ scope, messages, now }) {
|
|
171
|
+
const recentMessages = messages.slice(-RECENT_MESSAGE_COUNT);
|
|
172
|
+
const body = [
|
|
173
|
+
'Recent session details from the latest Dream pass:',
|
|
174
|
+
...recentMessages.map(m => `- ${m.id} ${m.role}${m.vpId ? `/${m.vpId}` : ''}: ${oneLine(m.body)}`),
|
|
175
|
+
].join('\n');
|
|
176
|
+
return makeSegment({
|
|
177
|
+
scope,
|
|
178
|
+
kind: 'context',
|
|
179
|
+
tags: ['recent', 'current'],
|
|
180
|
+
sourceMessages: recentMessages.map(m => m.id),
|
|
181
|
+
createdAt: now,
|
|
182
|
+
updatedAt: now,
|
|
183
|
+
body,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function mergeSegments(existing, incoming) {
|
|
188
|
+
const incomingRecent = incoming.filter(isRecentSegment).slice(-1);
|
|
189
|
+
const incomingPermanent = incoming.filter(seg => !isRecentSegment(seg));
|
|
190
|
+
const byKey = new Map();
|
|
191
|
+
|
|
192
|
+
for (const seg of existing) {
|
|
193
|
+
if (isRecentSegment(seg)) continue;
|
|
194
|
+
byKey.set(segmentMergeKey(seg), seg);
|
|
195
|
+
}
|
|
196
|
+
for (const seg of incomingPermanent) {
|
|
197
|
+
byKey.set(segmentMergeKey(seg), seg);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const permanent = [...byKey.values()]
|
|
201
|
+
.sort((a, b) => segmentTime(b).localeCompare(segmentTime(a)))
|
|
202
|
+
.slice(0, MAX_SEGMENTS_PER_SCOPE)
|
|
203
|
+
.sort((a, b) => segmentTime(a).localeCompare(segmentTime(b)) || String(a.id).localeCompare(String(b.id)));
|
|
204
|
+
|
|
205
|
+
return [...permanent, ...incomingRecent];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function segmentMergeKey(seg) {
|
|
209
|
+
const sources = Array.isArray(seg.sourceMessages)
|
|
210
|
+
? seg.sourceMessages.map(String).filter(Boolean).sort().join(',')
|
|
211
|
+
: '';
|
|
212
|
+
const tagFamily = Array.isArray(seg.tags)
|
|
213
|
+
? seg.tags.map(String).filter(t => t && t !== 'recent' && t !== 'current').sort().join(',')
|
|
214
|
+
: '';
|
|
215
|
+
if (sources) return `src:${seg.kind || 'context'}:${tagFamily}:${sources}`;
|
|
216
|
+
return `id:${seg.id}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function isRecentSegment(seg) {
|
|
220
|
+
return Array.isArray(seg.tags) && seg.tags.includes('recent');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function segmentTime(seg) {
|
|
224
|
+
return String(seg.updatedAt || seg.createdAt || '');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function rawSnippet(raw) {
|
|
228
|
+
return String(raw || '').replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function oneLine(text) {
|
|
232
|
+
return String(text || '').replace(/\s+/g, ' ').trim().slice(0, 280);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function extractSystem(language) {
|
|
236
|
+
return String(language || '').toLowerCase().startsWith('zh')
|
|
237
|
+
? '你是 Yeaft Dream 记忆抽取器。只输出严格 JSON 数组,不要 Markdown。保留具体事实、决策、偏好、当前状态和证据 message id。'
|
|
238
|
+
: 'You are the Yeaft Dream memory extractor. Return only a strict JSON array, no Markdown. Preserve concrete facts, decisions, preferences, current status, and evidence message ids.';
|
|
239
|
+
}
|
|
@@ -61,10 +61,26 @@ export function buildRunDreamOpts(session, onProgress) {
|
|
|
61
61
|
// Legacy disk fallback for pre-session transcript directories. New writes and
|
|
62
62
|
// Dream's primary source use `sessions/<sessionId>/conversation`.
|
|
63
63
|
const legacySessionConversationsRoot = join(yeaftDir, 'groups');
|
|
64
|
+
const loadSessionDiff = async (sessionId, sinceId) => {
|
|
65
|
+
try {
|
|
66
|
+
const messages = loadSessionConversationMessages([sessionConversationsRoot, legacySessionConversationsRoot], sessionId);
|
|
67
|
+
const out = [];
|
|
68
|
+
let started = !sinceId;
|
|
69
|
+
for (const m of messages) {
|
|
70
|
+
if (!started) {
|
|
71
|
+
if (m.id === sinceId) started = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
out.push(translateSessionConversationMessage(m));
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
} catch { return []; }
|
|
78
|
+
};
|
|
64
79
|
|
|
65
80
|
return {
|
|
66
81
|
root: memoryRoot,
|
|
67
82
|
language: session.config?.language || 'en',
|
|
83
|
+
segmentIndex: session.memoryIndex || null,
|
|
68
84
|
llm: makeLlm(session),
|
|
69
85
|
listSessions: async () => {
|
|
70
86
|
try { return listConversationSessions([sessionConversationsRoot, legacySessionConversationsRoot]); }
|
|
@@ -74,21 +90,9 @@ export function buildRunDreamOpts(session, onProgress) {
|
|
|
74
90
|
try { return loadSessionConversationMessages([sessionConversationsRoot, legacySessionConversationsRoot], sessionId).length; }
|
|
75
91
|
catch { return 0; }
|
|
76
92
|
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const out = [];
|
|
81
|
-
let started = !sinceId;
|
|
82
|
-
for (const m of messages) {
|
|
83
|
-
if (!started) {
|
|
84
|
-
if (m.id === sinceId) started = true;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
out.push(translateSessionConversationMessage(m));
|
|
88
|
-
}
|
|
89
|
-
return out;
|
|
90
|
-
} catch { return []; }
|
|
91
|
-
},
|
|
93
|
+
loadSessionDiff,
|
|
94
|
+
/** Legacy alias for older Dream runner callers; primary name is loadSessionDiff. */
|
|
95
|
+
loadGroupDiff: loadSessionDiff,
|
|
92
96
|
loadOverlapPreamble: async (sessionId, beforeId, n) => {
|
|
93
97
|
try {
|
|
94
98
|
const messages = loadSessionConversationMessages([sessionConversationsRoot, legacySessionConversationsRoot], sessionId);
|
package/yeaft/engine.js
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { randomUUID } from 'crypto';
|
|
21
|
-
import {
|
|
21
|
+
import { promises as fsp } from 'fs';
|
|
22
|
+
import { join, resolve as resolvePath } from 'path';
|
|
22
23
|
import { buildSystemPrompt, buildWorkerPrompt } from './prompts.js';
|
|
23
24
|
import { LLMContextError, LLMAbortError } from './llm/adapter.js';
|
|
24
25
|
import { runMemoryPreflow, buildRelevantScopes } from './sessions/pre-flow.js';
|
|
@@ -596,6 +597,14 @@ export class Engine {
|
|
|
596
597
|
return { user: user || '', session: session || '', vp: vp || '' };
|
|
597
598
|
}
|
|
598
599
|
|
|
600
|
+
async #loadSessionTopicLabels(sessionId, limit = 8) {
|
|
601
|
+
if (!this.#yeaftDir || !sessionId) return [];
|
|
602
|
+
const topicRoot = join(this.#yeaftDir, 'memory', 'sessions', sessionId, 'topic');
|
|
603
|
+
const labels = [];
|
|
604
|
+
await collectTopicLabels(topicRoot, '', labels, limit).catch(() => {});
|
|
605
|
+
return labels;
|
|
606
|
+
}
|
|
607
|
+
|
|
599
608
|
/**
|
|
600
609
|
* Prepare the per-turn AMS for the active group. Idempotent and safe
|
|
601
610
|
* to call when the AMS registry isn't wired (returns null).
|
|
@@ -1338,7 +1347,7 @@ export class Engine {
|
|
|
1338
1347
|
* string-prompt shape (no regression for existing callers).
|
|
1339
1348
|
* @yields {EngineEvent}
|
|
1340
1349
|
*/
|
|
1341
|
-
async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null, collabToolPolicy = null } = {}) {
|
|
1350
|
+
async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null, collabToolPolicy = null } = {}) {
|
|
1342
1351
|
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
1343
1352
|
yield {
|
|
1344
1353
|
type: 'error',
|
|
@@ -1401,7 +1410,7 @@ export class Engine {
|
|
|
1401
1410
|
|
|
1402
1411
|
try {
|
|
1403
1412
|
this.#currentThreadId = threadId || MAIN_THREAD_ID;
|
|
1404
|
-
yield* this.#runQuery({ prompt: effectivePrompt, promptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, threadId: this.#currentThreadId, drainPendingUserMessages, collabToolPolicy: effectiveCollabToolPolicy });
|
|
1413
|
+
yield* this.#runQuery({ prompt: effectivePrompt, promptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, threadId: this.#currentThreadId, drainPendingUserMessages, collabToolPolicy: effectiveCollabToolPolicy });
|
|
1405
1414
|
} finally {
|
|
1406
1415
|
if (signal) {
|
|
1407
1416
|
try { signal.removeEventListener('abort', onExternalAbort); } catch { /* ignore */ }
|
|
@@ -1421,7 +1430,7 @@ export class Engine {
|
|
|
1421
1430
|
* in a try/finally without indenting the whole loop.
|
|
1422
1431
|
* @private
|
|
1423
1432
|
*/
|
|
1424
|
-
async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null, collabToolPolicy = null }) {
|
|
1433
|
+
async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null, collabToolPolicy = null }) {
|
|
1425
1434
|
|
|
1426
1435
|
const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
|
|
1427
1436
|
? collabToolPolicy
|
|
@@ -1513,10 +1522,14 @@ export class Engine {
|
|
|
1513
1522
|
// Structured per-turn scope summary: session + vp + members + envelope routing
|
|
1514
1523
|
// info. Long-form scope content lives in AMS — this block carries
|
|
1515
1524
|
// only IDs + tiny labels. (Feature scope retired 2026-05-13.)
|
|
1525
|
+
const activeSessionTopics = Array.isArray(sessionTopics)
|
|
1526
|
+
? sessionTopics
|
|
1527
|
+
: await this.#loadSessionTopicLabels(sessionId);
|
|
1516
1528
|
const activeScope = {
|
|
1517
1529
|
sessionId: sessionId || '',
|
|
1518
|
-
|
|
1519
|
-
|
|
1530
|
+
sessionMember: ownVpIdForAms || '',
|
|
1531
|
+
sessionMembers: Array.isArray(sessionMembers) ? sessionMembers : [],
|
|
1532
|
+
sessionTopics: activeSessionTopics,
|
|
1520
1533
|
envelope: inboundEnvelope || null,
|
|
1521
1534
|
};
|
|
1522
1535
|
|
|
@@ -2907,3 +2920,22 @@ export class Engine {
|
|
|
2907
2920
|
}
|
|
2908
2921
|
}
|
|
2909
2922
|
}
|
|
2923
|
+
|
|
2924
|
+
async function collectTopicLabels(dir, prefix, labels, limit) {
|
|
2925
|
+
if (labels.length >= limit) return;
|
|
2926
|
+
let entries;
|
|
2927
|
+
try { entries = await fsp.readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
2928
|
+
const hasMemory = entries.some(entry => entry.isFile() && entry.name === 'memory.md');
|
|
2929
|
+
const hasSummary = entries.some(entry => entry.isFile() && entry.name === 'summary.md');
|
|
2930
|
+
if (prefix && (hasMemory || hasSummary)) labels.push(prefix);
|
|
2931
|
+
if (labels.length >= limit) return;
|
|
2932
|
+
const dirs = entries
|
|
2933
|
+
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
2934
|
+
.map(entry => entry.name)
|
|
2935
|
+
.sort();
|
|
2936
|
+
for (const name of dirs) {
|
|
2937
|
+
const nextPrefix = prefix ? `${prefix}/${name}` : name;
|
|
2938
|
+
await collectTopicLabels(join(dir, name), nextPrefix, labels, limit);
|
|
2939
|
+
if (labels.length >= limit) return;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
package/yeaft/memory/segment.js
CHANGED
|
@@ -51,7 +51,7 @@ export const KIND_VALUES = new Set([
|
|
|
51
51
|
'fact', 'preference', 'decision', 'lesson', 'relation', 'goal', 'context',
|
|
52
52
|
]);
|
|
53
53
|
|
|
54
|
-
const SCOPE_RE = /^(user|group\/[\w-]+(?:\/(?:user|vp\/[\w-]+|feature\/[\w-]+|topic\/[\w-]+(?:\/[\w-]+)?))?|chat\/[\w-]+(?:\/vp\/[\w-]+)?|session\/[\w-]+(?:\/vp\/[\w-]+)?)$/;
|
|
54
|
+
const SCOPE_RE = /^(user|group\/[\w-]+(?:\/(?:user|vp\/[\w-]+|feature\/[\w-]+|topic\/[\w-]+(?:\/[\w-]+)?))?|sessions\/[\w-]+(?:\/(?:user|vp\/[\w-]+|feature\/[\w-]+|topic\/[\w-]+(?:\/[\w-]+)?))?|chat\/[\w-]+(?:\/vp\/[\w-]+)?|session\/[\w-]+(?:\/vp\/[\w-]+)?)$/;
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* Compute a stable id from segment content. Same body + scope + kind →
|
package/yeaft/prompts.js
CHANGED
|
@@ -272,8 +272,9 @@ export function normalizePromptLanguage(language) {
|
|
|
272
272
|
* Active Scope params (DESIGN-PROMPT §3 ④):
|
|
273
273
|
* @param {object} [activeScope] — structured scope summary for this turn
|
|
274
274
|
* @param {string} [activeScope.sessionId]
|
|
275
|
-
* @param {string} [activeScope.
|
|
276
|
-
* @param {string[]} [activeScope.
|
|
275
|
+
* @param {string} [activeScope.sessionMember]
|
|
276
|
+
* @param {string[]} [activeScope.sessionMembers] current session roster
|
|
277
|
+
* @param {string[]} [activeScope.sessionTopics] bounded topic labels for this session
|
|
277
278
|
* @param {object} [activeScope.envelope] inbound routing info (sender, intent)
|
|
278
279
|
*
|
|
279
280
|
* @param {{
|
|
@@ -571,10 +572,11 @@ const OMNI_PERSONA_ZH = `你是全能助手,一个跨领域、偏执行的通
|
|
|
571
572
|
*
|
|
572
573
|
* Schema:
|
|
573
574
|
* ## active_scope
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
575
|
+
* session_id: <sessionId> (omitted when missing)
|
|
576
|
+
* session_member: <vpId> (omitted when missing)
|
|
577
|
+
* session_members: <vpId>, <vpId> (omitted when missing)
|
|
578
|
+
* session_topics: <topic>, <topic> (omitted when missing)
|
|
579
|
+
* envelope: from=<sender> intent=<intent> (omitted when no envelope)
|
|
578
580
|
*
|
|
579
581
|
* Returns '' when the input has no useful field — we don't emit an empty
|
|
580
582
|
* header. (`featureId`/`featureTitle` fields were removed 2026-05-13 along
|
|
@@ -582,8 +584,9 @@ const OMNI_PERSONA_ZH = `你是全能助手,一个跨领域、偏执行的通
|
|
|
582
584
|
*
|
|
583
585
|
* @param {object} [activeScope]
|
|
584
586
|
* @param {string} [activeScope.sessionId]
|
|
585
|
-
* @param {string} [activeScope.
|
|
586
|
-
* @param {string[]} [activeScope.
|
|
587
|
+
* @param {string} [activeScope.sessionMember]
|
|
588
|
+
* @param {string[]} [activeScope.sessionMembers] current session roster
|
|
589
|
+
* @param {string[]} [activeScope.sessionTopics] bounded topic labels
|
|
587
590
|
* @param {object} [activeScope.envelope] inbound routing summary
|
|
588
591
|
* @param {object} lang
|
|
589
592
|
* @returns {string}
|
|
@@ -595,15 +598,16 @@ function renderActiveScope(activeScope, lang) {
|
|
|
595
598
|
const session = typeof activeScope.sessionId === 'string' && activeScope.sessionId.trim()
|
|
596
599
|
? activeScope.sessionId.trim()
|
|
597
600
|
: '';
|
|
598
|
-
if (session) lines.push(`
|
|
601
|
+
if (session) lines.push(`session_id: ${session}`);
|
|
599
602
|
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
603
|
+
const sessionMember = firstNonEmptyString(activeScope.sessionMember, activeScope.vpId);
|
|
604
|
+
if (sessionMember) lines.push(`session_member: ${sessionMember}`);
|
|
605
|
+
|
|
606
|
+
const membersLine = renderSessionMembersLine(activeScope.sessionMembers || activeScope.members);
|
|
607
|
+
if (membersLine) lines.push(`session_members: ${membersLine}`);
|
|
604
608
|
|
|
605
|
-
const
|
|
606
|
-
if (
|
|
609
|
+
const topicsLine = renderSessionMembersLine(activeScope.sessionTopics);
|
|
610
|
+
if (topicsLine) lines.push(`session_topics: ${topicsLine}`);
|
|
607
611
|
|
|
608
612
|
const envLine = renderEnvelopeLine(activeScope.envelope);
|
|
609
613
|
if (envLine) lines.push(`envelope: ${envLine}`);
|
|
@@ -614,6 +618,13 @@ function renderActiveScope(activeScope, lang) {
|
|
|
614
618
|
}
|
|
615
619
|
|
|
616
620
|
|
|
621
|
+
function firstNonEmptyString(...values) {
|
|
622
|
+
for (const value of values) {
|
|
623
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
624
|
+
}
|
|
625
|
+
return '';
|
|
626
|
+
}
|
|
627
|
+
|
|
617
628
|
function renderSessionMembersLine(members) {
|
|
618
629
|
if (!Array.isArray(members)) return '';
|
|
619
630
|
const clean = [];
|