@yeaft/webchat-agent 1.0.348 → 1.0.350
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/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +73 -73
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
- package/yeaft/conversation/persist.js +4 -0
- package/yeaft/dream/apply.js +59 -30
- package/yeaft/dream/output-snapshot.js +8 -4
- package/yeaft/dream/prompts/consolidate-topics.md +31 -0
- package/yeaft/dream/prompts/create.md +8 -8
- package/yeaft/dream/prompts/index.js +4 -2
- package/yeaft/dream/prompts/merge-topics.md +35 -0
- package/yeaft/dream/prompts/triage-pass1.md +2 -2
- package/yeaft/dream/prompts/triage-pass2.md +4 -2
- package/yeaft/dream/prompts/update.md +16 -14
- package/yeaft/dream/runner.js +69 -13
- package/yeaft/dream/segment-extract.js +16 -13
- package/yeaft/dream/session-wiring.js +2 -2
- package/yeaft/dream/snapshot.js +3 -3
- package/yeaft/dream/topic-consolidation.js +316 -0
- package/yeaft/dream/triage.js +7 -2
- package/yeaft/engine.js +237 -239
- package/yeaft/memory/ams-registry.js +42 -61
- package/yeaft/memory/ams.js +17 -9
- package/yeaft/memory/budget.js +15 -18
- package/yeaft/memory/content-backfill.js +118 -0
- package/yeaft/memory/index-db.js +10 -3
- package/yeaft/memory/keywords.js +25 -7
- package/yeaft/memory/preflow.js +26 -9
- package/yeaft/memory/segment-store.js +44 -8
- package/yeaft/memory/segment-sync.js +8 -4
- package/yeaft/memory/segment.js +10 -3
- package/yeaft/memory/store.js +88 -38
- package/yeaft/memory/summary-store.js +3 -3
- package/yeaft/memory/topic-redirect.js +28 -0
- package/yeaft/router/continuity.js +12 -3
- package/yeaft/session.js +18 -19
- package/yeaft/sessions/pre-flow.js +7 -3
- package/yeaft/sub-agent/runner.js +10 -1
- package/yeaft/work-center/bridge.js +1 -0
- package/yeaft/work-center/runner.js +33 -4
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* memory/segment-store.js — disk I/O for segment
|
|
2
|
+
* memory/segment-store.js — disk I/O for segment evidence in memory.md.
|
|
3
3
|
*
|
|
4
|
-
* Bridges between the on-disk format (memory.md per scope, multiple
|
|
5
|
-
* segment blocks) and the SQLite segment index.
|
|
6
|
-
*
|
|
4
|
+
* Bridges between the on-disk evidence format (memory.md per scope, multiple
|
|
5
|
+
* segment blocks) and the SQLite segment index. Canonical prompt-facing prose
|
|
6
|
+
* is stored separately in content.md. This layer handles scope <-> file path
|
|
7
|
+
* mapping; the index layer is scope-agnostic.
|
|
7
8
|
*
|
|
8
9
|
* Path conventions:
|
|
9
10
|
* ~/.yeaft/memory/user/memory.md
|
|
@@ -18,8 +19,10 @@ import {
|
|
|
18
19
|
readFileSync, writeFileSync, existsSync, mkdirSync,
|
|
19
20
|
readdirSync, statSync, renameSync,
|
|
20
21
|
} from 'node:fs';
|
|
22
|
+
import { createHash } from 'node:crypto';
|
|
21
23
|
import { join, dirname, relative, sep } from 'node:path';
|
|
22
24
|
import { stripDreamStateBlocks } from './prompt-cleanup.js';
|
|
25
|
+
import { extractKeywords } from './keywords.js';
|
|
23
26
|
import { parseSegments, serializeSegments } from './segment.js';
|
|
24
27
|
|
|
25
28
|
/**
|
|
@@ -33,7 +36,9 @@ export function readScope(memoryRoot, scope) {
|
|
|
33
36
|
const path = scopeFilePath(memoryRoot, scope);
|
|
34
37
|
if (!existsSync(path)) return [];
|
|
35
38
|
const text = readFileSync(path, 'utf8');
|
|
36
|
-
|
|
39
|
+
const stripped = stripDreamStateBlocks(text);
|
|
40
|
+
if (!stripped.trimStart().startsWith('---')) return [];
|
|
41
|
+
return parseSegments(stripped, { defaultScope: scope });
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
/**
|
|
@@ -56,7 +61,37 @@ export function writeScope(memoryRoot, scope, segments) {
|
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
/**
|
|
59
|
-
*
|
|
64
|
+
* Read canonical content as a derived FTS record. The stable id lets normal
|
|
65
|
+
* sync delete or update it without changing the segment schema. This record is
|
|
66
|
+
* a scope selector only; Engine always reloads prompt text from content.md.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} memoryRoot
|
|
69
|
+
* @param {string} scope
|
|
70
|
+
* @returns {import('./segment.js').Segment|null}
|
|
71
|
+
*/
|
|
72
|
+
export function readCanonicalContentRecord(memoryRoot, scope) {
|
|
73
|
+
const path = join(memoryRoot, scope, 'content.md');
|
|
74
|
+
if (!existsSync(path)) return null;
|
|
75
|
+
const body = readFileSync(path, 'utf8').trim();
|
|
76
|
+
if (!body) return null;
|
|
77
|
+
const stat = statSync(path);
|
|
78
|
+
const timestamp = stat.mtime.toISOString();
|
|
79
|
+
const digest = createHash('sha256').update(scope).digest('hex').slice(0, 12);
|
|
80
|
+
return {
|
|
81
|
+
id: `content_${digest}`,
|
|
82
|
+
scope,
|
|
83
|
+
kind: 'context',
|
|
84
|
+
tags: ['canonical-content', ...extractKeywords(`${scope} ${body}`).slice(0, 128)],
|
|
85
|
+
sourceMessages: [],
|
|
86
|
+
createdAt: timestamp,
|
|
87
|
+
updatedAt: timestamp,
|
|
88
|
+
body,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Walk the memory root and return all scopes that have evidence memory.md or
|
|
94
|
+
* canonical content.md. Either representation must be queryable.
|
|
60
95
|
*
|
|
61
96
|
* @param {string} memoryRoot
|
|
62
97
|
* @returns {string[]}
|
|
@@ -74,10 +109,11 @@ function walk(root, dir, out) {
|
|
|
74
109
|
let st;
|
|
75
110
|
try { st = statSync(full); } catch { continue; }
|
|
76
111
|
if (st.isDirectory()) {
|
|
112
|
+
if (entry.startsWith('.')) continue;
|
|
77
113
|
walk(root, full, out);
|
|
78
|
-
} else if (entry === 'memory.md') {
|
|
114
|
+
} else if (entry === 'memory.md' || entry === 'content.md') {
|
|
79
115
|
const rel = relative(root, dir).split(sep).join('/');
|
|
80
|
-
if (rel) out.push(rel);
|
|
116
|
+
if (rel && !out.includes(rel)) out.push(rel);
|
|
81
117
|
}
|
|
82
118
|
}
|
|
83
119
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* memory/segment-sync.js — disk → SQLite reconciliation.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* index. This module reads disk, diffs against SQLite, and emits
|
|
4
|
+
* Sources of truth are on-disk memory.md evidence and canonical content.md.
|
|
5
|
+
* SQLite is a derived index. This module reads disk, diffs against SQLite, and emits
|
|
6
6
|
* upsert / delete operations.
|
|
7
7
|
*
|
|
8
8
|
* Strategy:
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* scope to limit work (`syncScope`).
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { listScopes, readScope } from './segment-store.js';
|
|
19
|
+
import { listScopes, readCanonicalContentRecord, readScope } from './segment-store.js';
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Full sync: walk disk, reconcile every scope into the index. Returns
|
|
@@ -61,7 +61,11 @@ export function syncAll(memoryRoot, index) {
|
|
|
61
61
|
* @returns {{ upserted: number, deleted: number }}
|
|
62
62
|
*/
|
|
63
63
|
export function syncScope(memoryRoot, index, scope) {
|
|
64
|
-
const
|
|
64
|
+
const contentRecord = readCanonicalContentRecord(memoryRoot, scope);
|
|
65
|
+
const onDisk = [
|
|
66
|
+
...readScope(memoryRoot, scope),
|
|
67
|
+
...(contentRecord ? [contentRecord] : []),
|
|
68
|
+
];
|
|
65
69
|
const onDiskIds = new Set(onDisk.map(s => s.id));
|
|
66
70
|
const inIndex = index.listByScope(scope);
|
|
67
71
|
const inIndexIds = new Set(inIndex.map(s => s.id));
|
package/yeaft/memory/segment.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* a self-contained semantic chunk (one segment per topic). NOT a copy
|
|
6
6
|
* of messages — messages already live in conversation/messages/.
|
|
7
7
|
*
|
|
8
|
-
* Physical layout: each scope's `memory.md` is
|
|
9
|
-
* concatenated
|
|
8
|
+
* Physical layout: each scope's `memory.md` is the evidence store containing
|
|
9
|
+
* multiple segments concatenated with YAML frontmatter. Prompt-facing canonical
|
|
10
|
+
* prose lives separately in `content.md`; segment bodies are never injected
|
|
11
|
+
* directly into the normal system prompt.
|
|
10
12
|
*
|
|
11
13
|
* ---
|
|
12
14
|
* id: seg_<8hex>
|
|
@@ -52,7 +54,12 @@ export const KIND_VALUES = new Set([
|
|
|
52
54
|
'workflow', 'pitfall', 'correction', 'project-convention',
|
|
53
55
|
]);
|
|
54
56
|
|
|
55
|
-
const
|
|
57
|
+
const TOPIC_PART = '[\\w.\\-\\u4e00-\\u9fff]+';
|
|
58
|
+
const SCOPE_RE = new RegExp(`^(user|global|group\\/[\\w-]+(?:\\/(?:user|vp\\/[\\w-]+|feature\\/[\\w-]+|topic\\/${TOPIC_PART}(?:\\/${TOPIC_PART})?))?|sessions\\/[\\w-]+(?:\\/(?:user|vp\\/[\\w-]+|feature\\/[\\w-]+|topic\\/${TOPIC_PART}(?:\\/${TOPIC_PART})?))?|chat\\/[\\w-]+(?:\\/vp\\/[\\w-]+)?|session\\/[\\w-]+(?:\\/vp\\/[\\w-]+)?)$`);
|
|
59
|
+
|
|
60
|
+
export function isValidSegmentScope(value) {
|
|
61
|
+
return typeof value === 'string' && SCOPE_RE.test(value);
|
|
62
|
+
}
|
|
56
63
|
|
|
57
64
|
/**
|
|
58
65
|
* Compute a stable id from segment content. Same body + scope + kind →
|
package/yeaft/memory/store.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* memory/store.js — per-scope
|
|
2
|
+
* memory/store.js — per-scope canonical content + summary storage.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Every scope can carry three distinct representations:
|
|
5
|
+
*
|
|
6
|
+
* - content.md: Dream-maintained canonical prose. This is the prompt-facing
|
|
7
|
+
* source and preserves all currently valid durable information.
|
|
8
|
+
* - summary.md: a short catalog / triage index. It is not authoritative.
|
|
9
|
+
* - memory.md: atomic evidence segments with source message ids. Segment I/O
|
|
10
|
+
* lives in segment-store.js; this module retains the legacy raw accessors.
|
|
7
11
|
*
|
|
8
12
|
* ~/.yeaft/memory/
|
|
9
|
-
* user/ memory.md summary.md
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* feature/<featureId>/ memory.md summary.md
|
|
13
|
-
* topic/<l1>[/<l2>]/ memory.md summary.md (≤ 2 levels)
|
|
13
|
+
* user/ content.md memory.md summary.md
|
|
14
|
+
* sessions/<sessionId>/ content.md memory.md summary.md
|
|
15
|
+
* .../topic/<l1>[/<l2>]/ content.md memory.md summary.md (≤ 2 levels)
|
|
14
16
|
*
|
|
15
17
|
* Atomicity contract:
|
|
16
18
|
* - Every write goes via `.tmp.<rand>` + rename. Renames are atomic on a
|
|
@@ -21,7 +23,7 @@
|
|
|
21
23
|
*
|
|
22
24
|
* Concurrency rules:
|
|
23
25
|
* - Two writers to the same memory.md: last-rename wins. Dream is the only
|
|
24
|
-
* code path that overwrites memory.md
|
|
26
|
+
* code path that overwrites memory.md; daily writes append. Append
|
|
25
27
|
* is a single fs.appendFile call that POSIX guarantees is atomic for
|
|
26
28
|
* buffers ≤ PIPE_BUF (≥ 4KB on every supported platform), which fits a
|
|
27
29
|
* single fragment.
|
|
@@ -50,11 +52,12 @@ import {
|
|
|
50
52
|
} from 'fs';
|
|
51
53
|
import { join, dirname } from 'path';
|
|
52
54
|
import { homedir } from 'os';
|
|
55
|
+
import { resolveTopicRedirect } from './topic-redirect.js';
|
|
53
56
|
|
|
54
57
|
/** Default memory root. Tests override via `opts.root`. */
|
|
55
58
|
export const DEFAULT_MEMORY_ROOT = join(homedir(), '.yeaft', 'memory');
|
|
56
59
|
|
|
57
|
-
/** Scope kinds recognised by
|
|
60
|
+
/** Scope kinds recognised by the current store, including legacy aliases. */
|
|
58
61
|
export const SCOPE_KINDS = Object.freeze([
|
|
59
62
|
'user',
|
|
60
63
|
'group',
|
|
@@ -89,10 +92,14 @@ export const SCOPE_KINDS = Object.freeze([
|
|
|
89
92
|
* @param {Scope} scope
|
|
90
93
|
* @returns {string}
|
|
91
94
|
*/
|
|
92
|
-
export function scopeDir(scope) {
|
|
95
|
+
export function scopeDir(scope, opts = {}) {
|
|
93
96
|
if (!scope || typeof scope !== 'object') {
|
|
94
97
|
throw new Error('scopeDir: scope is required');
|
|
95
98
|
}
|
|
99
|
+
if (scope.kind === 'session-topic' && opts.root) {
|
|
100
|
+
const redirected = resolveTopicRedirect(opts.root, scope.sessionId, (scope.path || []).join('/'));
|
|
101
|
+
if (redirected) scope = { ...scope, path: redirected.split('/') };
|
|
102
|
+
}
|
|
96
103
|
switch (scope.kind) {
|
|
97
104
|
case 'user':
|
|
98
105
|
return 'user';
|
|
@@ -230,9 +237,8 @@ export function isValidTopic(scope) {
|
|
|
230
237
|
// ─── ACL ───────────────────────────────────────────────────────
|
|
231
238
|
|
|
232
239
|
/**
|
|
233
|
-
* The single ACL:
|
|
234
|
-
*
|
|
235
|
-
* (the calling VP only runs inside its own group dir).
|
|
240
|
+
* The single ACL: a Session VP path owned by another VP is foreign when
|
|
241
|
+
* `currentVpId` is given. The `group/` spelling remains a legacy storage alias.
|
|
236
242
|
*
|
|
237
243
|
* @param {string} relPath
|
|
238
244
|
* @param {string} currentVpId
|
|
@@ -268,7 +274,46 @@ async function atomicWrite(absPath, content) {
|
|
|
268
274
|
await fsp.rename(tmp, absPath);
|
|
269
275
|
}
|
|
270
276
|
|
|
271
|
-
// ───
|
|
277
|
+
// ─── content.md ────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Read a scope's canonical Dream content. Missing → empty string.
|
|
281
|
+
*
|
|
282
|
+
* Deliberately does not fall back to memory.md: modern memory.md is the
|
|
283
|
+
* segment evidence store. Migration callers must make an explicit,
|
|
284
|
+
* format-aware fallback decision instead of injecting evidence blocks as prose.
|
|
285
|
+
*
|
|
286
|
+
* @param {Scope} scope
|
|
287
|
+
* @param {{ root?: string, currentVpId?: string }} [opts]
|
|
288
|
+
* @returns {Promise<string>}
|
|
289
|
+
*/
|
|
290
|
+
export async function readContent(scope, opts = {}) {
|
|
291
|
+
const { root = DEFAULT_MEMORY_ROOT, currentVpId } = opts;
|
|
292
|
+
const rel = `${scopeDir(scope, { root })}/content.md`;
|
|
293
|
+
enforceVpAcl(rel, currentVpId);
|
|
294
|
+
const abs = join(root, rel);
|
|
295
|
+
try { return await fsp.readFile(abs, 'utf8'); }
|
|
296
|
+
catch (err) {
|
|
297
|
+
if (err && err.code === 'ENOENT') return '';
|
|
298
|
+
throw err;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Atomically rewrite a scope's canonical Dream content.
|
|
304
|
+
*
|
|
305
|
+
* @param {Scope} scope
|
|
306
|
+
* @param {string} content
|
|
307
|
+
* @param {{ root?: string, currentVpId?: string }} [opts]
|
|
308
|
+
*/
|
|
309
|
+
export async function writeContent(scope, content, opts = {}) {
|
|
310
|
+
const { root = DEFAULT_MEMORY_ROOT, currentVpId } = opts;
|
|
311
|
+
const rel = `${scopeDir(scope, { root })}/content.md`;
|
|
312
|
+
enforceVpAcl(rel, currentVpId);
|
|
313
|
+
await atomicWrite(join(root, rel), typeof content === 'string' ? content : '');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ─── memory.md (segment evidence / legacy raw access) ──────────
|
|
272
317
|
|
|
273
318
|
/**
|
|
274
319
|
* Read a scope's memory.md. Missing → empty string.
|
|
@@ -279,7 +324,7 @@ async function atomicWrite(absPath, content) {
|
|
|
279
324
|
*/
|
|
280
325
|
export async function readMemory(scope, opts = {}) {
|
|
281
326
|
const { root = DEFAULT_MEMORY_ROOT, currentVpId } = opts;
|
|
282
|
-
const rel = `${scopeDir(scope)}/memory.md`;
|
|
327
|
+
const rel = `${scopeDir(scope, { root })}/memory.md`;
|
|
283
328
|
enforceVpAcl(rel, currentVpId);
|
|
284
329
|
const abs = join(root, rel);
|
|
285
330
|
try { return await fsp.readFile(abs, 'utf8'); }
|
|
@@ -298,7 +343,7 @@ export async function readMemory(scope, opts = {}) {
|
|
|
298
343
|
*/
|
|
299
344
|
export async function writeMemory(scope, content, opts = {}) {
|
|
300
345
|
const { root = DEFAULT_MEMORY_ROOT, currentVpId } = opts;
|
|
301
|
-
const rel = `${scopeDir(scope)}/memory.md`;
|
|
346
|
+
const rel = `${scopeDir(scope, { root })}/memory.md`;
|
|
302
347
|
enforceVpAcl(rel, currentVpId);
|
|
303
348
|
const abs = join(root, rel);
|
|
304
349
|
await atomicWrite(abs, typeof content === 'string' ? content : '');
|
|
@@ -320,7 +365,7 @@ export async function writeMemory(scope, content, opts = {}) {
|
|
|
320
365
|
*/
|
|
321
366
|
export async function appendMemory(scope, chunk, opts = {}) {
|
|
322
367
|
const { root = DEFAULT_MEMORY_ROOT, currentVpId } = opts;
|
|
323
|
-
const rel = `${scopeDir(scope)}/memory.md`;
|
|
368
|
+
const rel = `${scopeDir(scope, { root })}/memory.md`;
|
|
324
369
|
enforceVpAcl(rel, currentVpId);
|
|
325
370
|
const abs = join(root, rel);
|
|
326
371
|
await fsp.mkdir(dirname(abs), { recursive: true });
|
|
@@ -337,8 +382,8 @@ function summaryFileName(language) {
|
|
|
337
382
|
return normalized === 'zh' ? 'summary.zh.md' : 'summary.md';
|
|
338
383
|
}
|
|
339
384
|
|
|
340
|
-
function summaryCandidateRels(scope, language) {
|
|
341
|
-
const dir = scopeDir(scope);
|
|
385
|
+
function summaryCandidateRels(scope, language, root) {
|
|
386
|
+
const dir = scopeDir(scope, { root });
|
|
342
387
|
const primary = `${dir}/${summaryFileName(language)}`;
|
|
343
388
|
const fallback = `${dir}/summary.md`;
|
|
344
389
|
return primary === fallback ? [fallback] : [primary, fallback];
|
|
@@ -353,7 +398,7 @@ function summaryCandidateRels(scope, language) {
|
|
|
353
398
|
*/
|
|
354
399
|
export async function readSummary(scope, opts = {}) {
|
|
355
400
|
const { root = DEFAULT_MEMORY_ROOT, currentVpId, language } = opts;
|
|
356
|
-
for (const rel of summaryCandidateRels(scope, language)) {
|
|
401
|
+
for (const rel of summaryCandidateRels(scope, language, root)) {
|
|
357
402
|
enforceVpAcl(rel, currentVpId);
|
|
358
403
|
const abs = join(root, rel);
|
|
359
404
|
try { return (await fsp.readFile(abs, 'utf8')).trim(); }
|
|
@@ -374,7 +419,7 @@ export async function readSummary(scope, opts = {}) {
|
|
|
374
419
|
*/
|
|
375
420
|
export async function writeSummary(scope, body, opts = {}) {
|
|
376
421
|
const { root = DEFAULT_MEMORY_ROOT, currentVpId, language } = opts;
|
|
377
|
-
const rel = `${scopeDir(scope)}/${summaryFileName(language)}`;
|
|
422
|
+
const rel = `${scopeDir(scope, { root })}/${summaryFileName(language)}`;
|
|
378
423
|
enforceVpAcl(rel, currentVpId);
|
|
379
424
|
const abs = join(root, rel);
|
|
380
425
|
await atomicWrite(abs, `${(body || '').trim()}\n`);
|
|
@@ -382,14 +427,12 @@ export async function writeSummary(scope, body, opts = {}) {
|
|
|
382
427
|
|
|
383
428
|
/**
|
|
384
429
|
* Seed a scope's summary.md if (and only if) it is missing or empty. Used
|
|
385
|
-
* at create-time for
|
|
386
|
-
*
|
|
387
|
-
* earlier behavior of "no summary.md until Dream-v2 runs" left the memory
|
|
388
|
-
* section empty for the entire first session.
|
|
430
|
+
* at create-time for legacy bootstrap and catalog readers. The prompt path no
|
|
431
|
+
* longer consumes these seeds; it selects canonical content.md instead.
|
|
389
432
|
*
|
|
390
433
|
* Intentionally a no-op if a non-empty summary.md already exists, so this
|
|
391
|
-
* is safe to call from any
|
|
392
|
-
*
|
|
434
|
+
* is safe to call from any existing scope-creation path without clobbering
|
|
435
|
+
* Dream's catalog writes.
|
|
393
436
|
*
|
|
394
437
|
* @param {Scope} scope
|
|
395
438
|
* @param {string} body
|
|
@@ -422,7 +465,7 @@ export async function seedSummaryIfMissing(scope, body, opts = {}) {
|
|
|
422
465
|
*/
|
|
423
466
|
export function seedSummaryIfMissingSync(scope, body, opts = {}) {
|
|
424
467
|
const { root = DEFAULT_MEMORY_ROOT } = opts;
|
|
425
|
-
const rel = `${scopeDir(scope)}/summary.md`;
|
|
468
|
+
const rel = `${scopeDir(scope, { root })}/summary.md`;
|
|
426
469
|
const abs = join(root, rel);
|
|
427
470
|
let existing = '';
|
|
428
471
|
if (existsSync(abs)) {
|
|
@@ -438,7 +481,7 @@ export function seedSummaryIfMissingSync(scope, body, opts = {}) {
|
|
|
438
481
|
/**
|
|
439
482
|
* Synchronously remove a scope's directory under the memory root. Used by
|
|
440
483
|
* `deleteVp` / `deleteSession` to cascade memory cleanup so a recreate of the
|
|
441
|
-
* same id doesn't see stale `
|
|
484
|
+
* same id doesn't see stale `content.md` / `summary.md` / `memory.md` files.
|
|
442
485
|
*
|
|
443
486
|
* Idempotent — missing directory is a no-op.
|
|
444
487
|
*
|
|
@@ -447,7 +490,7 @@ export function seedSummaryIfMissingSync(scope, body, opts = {}) {
|
|
|
447
490
|
*/
|
|
448
491
|
export function removeScopeDirSync(scope, opts = {}) {
|
|
449
492
|
const { root = DEFAULT_MEMORY_ROOT } = opts;
|
|
450
|
-
const abs = join(root, scopeDir(scope));
|
|
493
|
+
const abs = join(root, scopeDir(scope, { root }));
|
|
451
494
|
if (!existsSync(abs)) return;
|
|
452
495
|
rmSync(abs, { recursive: true, force: true });
|
|
453
496
|
}
|
|
@@ -462,7 +505,7 @@ export function removeScopeDirSync(scope, opts = {}) {
|
|
|
462
505
|
*/
|
|
463
506
|
export function ensureScopeSync(scope, opts = {}) {
|
|
464
507
|
const { root = DEFAULT_MEMORY_ROOT } = opts;
|
|
465
|
-
const dir = join(root, scopeDir(scope));
|
|
508
|
+
const dir = join(root, scopeDir(scope, { root }));
|
|
466
509
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
467
510
|
}
|
|
468
511
|
|
|
@@ -474,7 +517,7 @@ export function ensureScopeSync(scope, opts = {}) {
|
|
|
474
517
|
*/
|
|
475
518
|
export async function ensureScope(scope, opts = {}) {
|
|
476
519
|
const { root = DEFAULT_MEMORY_ROOT } = opts;
|
|
477
|
-
const dir = join(root, scopeDir(scope));
|
|
520
|
+
const dir = join(root, scopeDir(scope, { root }));
|
|
478
521
|
await fsp.mkdir(dir, { recursive: true });
|
|
479
522
|
}
|
|
480
523
|
|
|
@@ -488,7 +531,8 @@ export async function ensureScope(scope, opts = {}) {
|
|
|
488
531
|
* group/<g>/user/ → { kind: 'group-user', sessionId: g }
|
|
489
532
|
* group/<g>/vp/<v>/ → { kind: 'group-vp', sessionId: g, id: v }
|
|
490
533
|
* group/<g>/feature/<f>/ → { kind: 'group-feature', sessionId: g, id: f }
|
|
491
|
-
* group/<g>/topic/<l1>[/<l2>]/ →
|
|
534
|
+
* group/<g>/topic/<l1>[/<l2>]/ → legacy group-topic
|
|
535
|
+
* sessions/<s>/topic/<l1>[/<l2>]/ → { kind: 'session-topic', sessionId: s, path: [...] }
|
|
492
536
|
*
|
|
493
537
|
* Skips `.legacy/` and any dotfile / unsafe segment.
|
|
494
538
|
*
|
|
@@ -646,14 +690,20 @@ export async function listScopes(opts = {}) {
|
|
|
646
690
|
for (const tent of topics) {
|
|
647
691
|
if (!tent.isDirectory()) continue;
|
|
648
692
|
if (!isSafeId(tent.name)) continue;
|
|
649
|
-
|
|
693
|
+
const topicAbs = join(topicDir, tent.name);
|
|
650
694
|
let subTopics;
|
|
651
|
-
try { subTopics = await fsp.readdir(
|
|
695
|
+
try { subTopics = await fsp.readdir(topicAbs, { withFileTypes: true }); }
|
|
652
696
|
catch { subTopics = []; }
|
|
697
|
+
const hasOwnMemory = ['content.md', 'memory.md', 'summary.md', 'summary.zh.md']
|
|
698
|
+
.some(name => existsSync(join(topicAbs, name)));
|
|
699
|
+
if (hasOwnMemory) out.push({ kind: 'session-topic', sessionId: s, path: [tent.name] });
|
|
653
700
|
for (const sub of subTopics) {
|
|
654
701
|
if (!sub.isDirectory()) continue;
|
|
655
702
|
if (!isSafeId(sub.name)) continue;
|
|
656
|
-
|
|
703
|
+
const subAbs = join(topicAbs, sub.name);
|
|
704
|
+
const hasSubMemory = ['content.md', 'memory.md', 'summary.md', 'summary.zh.md']
|
|
705
|
+
.some(name => existsSync(join(subAbs, name)));
|
|
706
|
+
if (hasSubMemory) out.push({ kind: 'session-topic', sessionId: s, path: [tent.name, sub.name] });
|
|
657
707
|
}
|
|
658
708
|
}
|
|
659
709
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* memory/summary-store.js — DESIGN-H2-AMS §3.
|
|
3
3
|
*
|
|
4
|
-
* `summary.md` is a bounded
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* `summary.md` is a bounded catalog description used by Dream triage and
|
|
5
|
+
* topic consolidation. It is not authoritative and is not prompt content;
|
|
6
|
+
* canonical prose lives in content.md, while evidence lives in memory.md.
|
|
7
7
|
*
|
|
8
8
|
* Layout:
|
|
9
9
|
* ~/.yeaft/memory/<scope>/summary.md
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const TOPIC_REDIRECT_FILE = 'redirect.json';
|
|
5
|
+
|
|
6
|
+
export function normalizeTopicPath(value) {
|
|
7
|
+
const parts = String(value || '').split('/').map(part => part.trim()).filter(Boolean);
|
|
8
|
+
if (parts.length === 0 || parts.length > 2) return '';
|
|
9
|
+
if (parts.some(part => part === '.' || part === '..' || /[\\\0]/.test(part))) return '';
|
|
10
|
+
return parts.join('/');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveTopicRedirect(root, sessionId, path) {
|
|
14
|
+
let current = normalizeTopicPath(path);
|
|
15
|
+
const visited = new Set();
|
|
16
|
+
while (current && !visited.has(current)) {
|
|
17
|
+
visited.add(current);
|
|
18
|
+
const redirectPath = join(root, 'sessions', sessionId, 'topic', current, TOPIC_REDIRECT_FILE);
|
|
19
|
+
if (!existsSync(redirectPath)) break;
|
|
20
|
+
let payload;
|
|
21
|
+
try { payload = JSON.parse(readFileSync(redirectPath, 'utf8') || '{}'); }
|
|
22
|
+
catch { break; }
|
|
23
|
+
const next = normalizeTopicPath(payload?.canonical);
|
|
24
|
+
if (!next || next === current) break;
|
|
25
|
+
current = next;
|
|
26
|
+
}
|
|
27
|
+
return current;
|
|
28
|
+
}
|
|
@@ -86,7 +86,8 @@ export function extractPriorPlan(messages, vpId) {
|
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
88
|
* Return a copy of the messages array with engine-private metadata stripped
|
|
89
|
-
* from every message.
|
|
89
|
+
* from every message. This includes durable UI-only execution provenance.
|
|
90
|
+
* The serialisers (anthropic/openai-responses) read this;
|
|
90
91
|
* these fields are NEVER part of the wire payload. Cheap because we only
|
|
91
92
|
* shallow-clone the messages that actually have private fields.
|
|
92
93
|
*
|
|
@@ -99,9 +100,17 @@ export function stripMetaForWire(messages) {
|
|
|
99
100
|
const out = messages.map(m => {
|
|
100
101
|
if (m && typeof m === 'object'
|
|
101
102
|
&& ('_meta' in m || '_runtimeTurnId' in m || '_partialTurn' in m
|
|
102
|
-
|| '_persistedMessageId' in m || 'userAuthored' in m)) {
|
|
103
|
+
|| '_persistedMessageId' in m || 'userAuthored' in m || 'executionOrigin' in m)) {
|
|
103
104
|
mutated = true;
|
|
104
|
-
const {
|
|
105
|
+
const {
|
|
106
|
+
_meta,
|
|
107
|
+
_runtimeTurnId,
|
|
108
|
+
_partialTurn,
|
|
109
|
+
_persistedMessageId,
|
|
110
|
+
userAuthored,
|
|
111
|
+
executionOrigin,
|
|
112
|
+
...rest
|
|
113
|
+
} = m;
|
|
105
114
|
return rest;
|
|
106
115
|
}
|
|
107
116
|
return m;
|
package/yeaft/session.js
CHANGED
|
@@ -35,18 +35,16 @@ import { TaskManager } from './tasks/manager.js';
|
|
|
35
35
|
//
|
|
36
36
|
// GC.1 (final): the session opens a SegmentIndex (SQLite FTS5 over
|
|
37
37
|
// memory.md) and passes it to the Engine. Engine.#recallMemory routes
|
|
38
|
-
// pre-turn recall through
|
|
38
|
+
// pre-turn recall through sessions/pre-flow.js → memory/preflow.js (the
|
|
39
39
|
// previous per-scope file reader recall-v2.js has been deleted).
|
|
40
40
|
// The `config.memoryV2` opt-out flag was retired in task-710; wiring is
|
|
41
41
|
// unconditional.
|
|
42
42
|
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// disconnect. Engine.#runQuery uses the registry to populate the
|
|
49
|
-
// AMS each turn and to run `memory/adjust.js` post-turn.
|
|
43
|
+
// When memoryIndex is wired we also open an AmsRegistry. It caches the
|
|
44
|
+
// per-Session ActiveMemorySet object and keeps the version-1 ams.json shape for
|
|
45
|
+
// disk compatibility. Engine rebuilds prompt-facing Resident entries from
|
|
46
|
+
// query-selected canonical content on every turn; persisted segment ids are
|
|
47
|
+
// never rehydrated into the prompt.
|
|
50
48
|
import { ensureDefaultSessionIfEmpty, migrateRegisteredWorkDirSessions } from './sessions/session-crud.js';
|
|
51
49
|
import { seedDefaultVps } from './vp/seed-defaults.js';
|
|
52
50
|
import { topUpDefaultVps } from './vp/seed-topup.js';
|
|
@@ -54,6 +52,7 @@ import { archiveLegacyScopes } from './memory/seed-backfill.js';
|
|
|
54
52
|
import { createV2DreamScheduler, bootInitEmptyGroups, bootCatchUpStaleDream } from './dream/session-wiring.js';
|
|
55
53
|
import { openSegmentIndex } from './memory/index-db.js';
|
|
56
54
|
import { syncAll as syncSegmentIndex } from './memory/segment-sync.js';
|
|
55
|
+
import { backfillCanonicalContent } from './memory/content-backfill.js';
|
|
57
56
|
import { openAmsRegistry } from './memory/ams-registry.js';
|
|
58
57
|
import { join } from 'path';
|
|
59
58
|
import { existsSync as existsSyncSafe, readFileSync as readFileSyncSafe, mkdirSync as mkdirSyncSafe } from 'fs';
|
|
@@ -267,9 +266,10 @@ export async function loadSession(options = {}) {
|
|
|
267
266
|
const conversationStore = new ConversationStore(yeaftDir);
|
|
268
267
|
|
|
269
268
|
// ─── 5-fts. (GC.1) Open SegmentIndex for FTS pre-flow ────
|
|
270
|
-
// Build a SQLite FTS5 index over
|
|
271
|
-
//
|
|
272
|
-
//
|
|
269
|
+
// Build a SQLite FTS5 index over per-scope evidence memory.md and
|
|
270
|
+
// canonical content.md, then pass it to Engine for scope selection.
|
|
271
|
+
// Engine.#recallMemory uses it via sessions/pre-flow.js →
|
|
272
|
+
// memory/preflow.js. Disk is the source of
|
|
273
273
|
// truth; on boot we reconcile disk → index via syncAll. Failure
|
|
274
274
|
// to open the index is non-fatal: #recallMemory returns an empty
|
|
275
275
|
// result and the turn proceeds without pre-injected memory.
|
|
@@ -290,6 +290,7 @@ export async function loadSession(options = {}) {
|
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
292
|
try {
|
|
293
|
+
backfillCanonicalContent(memoryRoot);
|
|
293
294
|
syncSegmentIndex(memoryRoot, memoryIndex);
|
|
294
295
|
} catch (syncErr) {
|
|
295
296
|
// Sync is best-effort; an empty / partial index just produces
|
|
@@ -304,12 +305,11 @@ export async function loadSession(options = {}) {
|
|
|
304
305
|
}
|
|
305
306
|
}
|
|
306
307
|
|
|
307
|
-
// ─── 5-ams.
|
|
308
|
+
// ─── 5-ams. Session-keyed AMS registry ─────────────────
|
|
308
309
|
// The registry caches one ActiveMemorySet per sessionId and
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
// re-hydrate against, so the registry is left null in that case.
|
|
310
|
+
// retains version-1 metadata for disk compatibility. Prompt state is
|
|
311
|
+
// rebuilt from selected canonical content each turn; old segment ids are
|
|
312
|
+
// not rehydrated. Without memoryIndex the registry remains disabled.
|
|
313
313
|
let amsRegistry = null;
|
|
314
314
|
if (memoryIndex && !config._readOnly) {
|
|
315
315
|
try {
|
|
@@ -559,9 +559,8 @@ export async function loadSession(options = {}) {
|
|
|
559
559
|
|
|
560
560
|
// H2.f.5 retired the old session-level thread engine registry, input queue,
|
|
561
561
|
// and dispatcher. The session exposes a default `engine`; PR #797 keeps
|
|
562
|
-
//
|
|
563
|
-
// directly.
|
|
564
|
-
// memory/adjust.js (post-turn).
|
|
562
|
+
// Session VP thread engines in web-bridge runtime state and calls engine.query()
|
|
563
|
+
// directly. Query-time recall happens via memory/preflow.js.
|
|
565
564
|
|
|
566
565
|
// ─── 10. Build session ─────────────────────────────────
|
|
567
566
|
const status = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* sessions/pre-flow.js — explicit pre-flow stage for Yeaft.
|
|
3
3
|
*
|
|
4
4
|
* Pre-flow is the "before any VP runs" stage. It owns:
|
|
5
5
|
*
|
|
@@ -252,14 +252,16 @@ export function formatPickedForInjection(picked) {
|
|
|
252
252
|
/**
|
|
253
253
|
* @typedef {object} MemoryPreflowOptions
|
|
254
254
|
* @property {string} userMsg The user's message
|
|
255
|
-
* @property {string} [sessionId] Active
|
|
255
|
+
* @property {string} [sessionId] Active Session, if any
|
|
256
256
|
* @property {string} [vpId] Responding VP id, if any
|
|
257
257
|
* @property {string} [featureId] Active feature, if any
|
|
258
258
|
* @property {string[]} [extraScopes] Additional scopes to include
|
|
259
259
|
* @property {string[]} [currentTags] Contextual tags for rerank
|
|
260
|
-
* @property {number} [topK] Max FTS rows fetched (default
|
|
260
|
+
* @property {number} [topK] Max FTS rows fetched (default 200)
|
|
261
261
|
* @property {number} [budgetTokens] Token budget for picked segments
|
|
262
262
|
* @property {number} [pickLimit] Max picked segments (default 8)
|
|
263
|
+
* @property {boolean} [uniqueScopes] Pick only the best hit per scope
|
|
264
|
+
* @property {boolean} [canonicalOnly] Search canonical content records only
|
|
263
265
|
* @property {boolean} [fallbackOnEmpty] Include bounded recent scoped segments when FTS has no hits
|
|
264
266
|
* @property {number} [fallbackPerScope] Max fallback segments per scope
|
|
265
267
|
*/
|
|
@@ -355,6 +357,8 @@ export function runMemoryPreflow(index, opts) {
|
|
|
355
357
|
topK: opts.topK,
|
|
356
358
|
budgetTokens: opts.budgetTokens,
|
|
357
359
|
pickLimit: opts.pickLimit,
|
|
360
|
+
uniqueScopes: opts.uniqueScopes === true,
|
|
361
|
+
canonicalOnly: opts.canonicalOnly === true,
|
|
358
362
|
});
|
|
359
363
|
|
|
360
364
|
let fallbackUsed = false;
|
|
@@ -353,7 +353,16 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
|
|
|
353
353
|
// Seed: mission becomes the first user prompt.
|
|
354
354
|
if (!agent.pendingPrompts) agent.pendingPrompts = [];
|
|
355
355
|
if (agent.mission && !agent.__missionSeeded) {
|
|
356
|
-
agent.pendingPrompts.push(
|
|
356
|
+
agent.pendingPrompts.push({
|
|
357
|
+
prompt: agent.mission,
|
|
358
|
+
projectSessionIds: Array.isArray(deps.projectSessionIds)
|
|
359
|
+
? deps.projectSessionIds.slice()
|
|
360
|
+
: [],
|
|
361
|
+
projectLabel: typeof deps.projectLabel === 'string' ? deps.projectLabel : '',
|
|
362
|
+
projectInstruction: typeof deps.projectInstruction === 'string'
|
|
363
|
+
? deps.projectInstruction
|
|
364
|
+
: '',
|
|
365
|
+
});
|
|
357
366
|
agent.__missionSeeded = true;
|
|
358
367
|
}
|
|
359
368
|
|