@yeaft/webchat-agent 0.1.856 → 0.1.859
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/conversation.js +92 -28
- package/package.json +1 -1
- package/providers/base.js +34 -0
- package/providers/claude-code.js +34 -0
- package/providers/copilot.js +278 -0
- package/providers/index.js +19 -0
- package/yeaft/compact/orchestrator.js +7 -13
- package/yeaft/conversation/persist.js +94 -53
- package/yeaft/conversation/search.js +24 -6
- package/yeaft/dream-v2/apply.js +28 -11
- package/yeaft/dream-v2/prompts/index.js +8 -1
- package/yeaft/dream-v2/runner.js +12 -5
- package/yeaft/dream-v2/triage.js +13 -8
- package/yeaft/engine.js +2 -2
- package/yeaft/groups/pre-flow.js +14 -2
- package/yeaft/init.js +6 -11
- package/yeaft/memory/adjust.js +2 -4
- package/yeaft/memory/ams.js +2 -4
- package/yeaft/memory/preflow.js +2 -6
- package/yeaft/memory/seed-backfill.js +41 -1
- package/yeaft/memory/segment.js +1 -1
- package/yeaft/memory/store-v2.js +120 -73
- package/yeaft/session.js +11 -1
- package/yeaft/vp/vp-crud.js +7 -24
package/yeaft/init.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* init.js — Yeaft directory structure initialization
|
|
3
3
|
*
|
|
4
4
|
* Ensures ~/.yeaft/ and all required subdirectories exist.
|
|
5
|
-
* Creates default config.md, MEMORY.md,
|
|
5
|
+
* Creates default config.md, MEMORY.md, and chat/index.md if missing.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync, mkdirSync, writeFileSync, accessSync, constants } from 'fs';
|
|
@@ -77,10 +77,7 @@ const SUBDIRS = [
|
|
|
77
77
|
'chat/messages',
|
|
78
78
|
'chat/cold',
|
|
79
79
|
'chat/blobs',
|
|
80
|
-
'
|
|
81
|
-
'group/cold',
|
|
82
|
-
'group/blobs',
|
|
83
|
-
'group/compact',
|
|
80
|
+
'groups',
|
|
84
81
|
'memory/entries',
|
|
85
82
|
'tasks',
|
|
86
83
|
'skills',
|
|
@@ -200,12 +197,10 @@ export function initYeaftDir(dir) {
|
|
|
200
197
|
created.push(memoryPath);
|
|
201
198
|
}
|
|
202
199
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
created.push(indexPath);
|
|
208
|
-
}
|
|
200
|
+
const chatIndexPath = join(root, 'chat', 'index.md');
|
|
201
|
+
if (!existsSync(chatIndexPath)) {
|
|
202
|
+
safeWriteFile(chatIndexPath, DEFAULT_CONVERSATION_INDEX, warnings);
|
|
203
|
+
created.push(chatIndexPath);
|
|
209
204
|
}
|
|
210
205
|
|
|
211
206
|
// mcp.json.example — reference template for MCP server configuration
|
package/yeaft/memory/adjust.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { approxTokens } from './budget.js';
|
|
31
|
+
import { isVpForeign } from './store-v2.js';
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* @typedef {object} AdjustTriggerInput
|
|
@@ -104,10 +105,7 @@ function firstSentence(body) {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
function isOwnOrNonVp(scope, ownVpId) {
|
|
107
|
-
|
|
108
|
-
if (!ownVpId) return true;
|
|
109
|
-
const other = scope.slice(3).split('/')[0];
|
|
110
|
-
return other === ownVpId;
|
|
108
|
+
return !isVpForeign(scope, ownVpId);
|
|
111
109
|
}
|
|
112
110
|
|
|
113
111
|
/**
|
package/yeaft/memory/ams.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { approxTokens, packWithinBudget } from './budget.js';
|
|
20
|
+
import { isVpForeign } from './store-v2.js';
|
|
20
21
|
|
|
21
22
|
const RECENT_DEFAULT_CAPACITY = 64;
|
|
22
23
|
|
|
@@ -189,9 +190,6 @@ export class ActiveMemorySet {
|
|
|
189
190
|
// ────────────────────────── privacy ──────────────────────────
|
|
190
191
|
|
|
191
192
|
_isForeignVp(scope) {
|
|
192
|
-
|
|
193
|
-
if (!this.ownVpId) return false; // no own id → no filtering
|
|
194
|
-
const other = scope.slice(3).split('/')[0];
|
|
195
|
-
return other !== this.ownVpId;
|
|
193
|
+
return isVpForeign(scope, this.ownVpId);
|
|
196
194
|
}
|
|
197
195
|
}
|
package/yeaft/memory/preflow.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { extractKeywords } from './keywords.js';
|
|
19
19
|
import { approxTokens } from './budget.js';
|
|
20
|
+
import { isVpForeign } from './store-v2.js';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* @typedef {object} PreflowOptions
|
|
@@ -118,12 +119,7 @@ export function buildFtsQuery(keywords) {
|
|
|
118
119
|
* @returns {string[]}
|
|
119
120
|
*/
|
|
120
121
|
export function filterScopes(scopes, ownVpId) {
|
|
121
|
-
return scopes.filter(s =>
|
|
122
|
-
if (!s.startsWith('vp/')) return true;
|
|
123
|
-
if (!ownVpId) return true;
|
|
124
|
-
const other = s.slice(3).split('/')[0];
|
|
125
|
-
return other === ownVpId;
|
|
126
|
-
});
|
|
122
|
+
return scopes.filter(s => !isVpForeign(s, ownVpId));
|
|
127
123
|
}
|
|
128
124
|
|
|
129
125
|
/**
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* a permission error must NEVER prevent the session from loading.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync, renameSync } from 'fs';
|
|
19
19
|
import { join } from 'path';
|
|
20
20
|
import { homedir } from 'os';
|
|
21
21
|
import { parseRoleMd } from '../vp/vp-store.js';
|
|
@@ -292,3 +292,43 @@ export function runSummaryBackfill({ yeaftDir, libDir, root = DEFAULT_MEMORY_ROO
|
|
|
292
292
|
}
|
|
293
293
|
return { migrate, vp, group };
|
|
294
294
|
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* archiveLegacyScopes(root) — one-shot migration for the group-isolated
|
|
298
|
+
* memory refactor. The legacy flat layout had `vp/<id>/`, `feature/<id>/`,
|
|
299
|
+
* and `topic/<l1>[/<l2>]/` directories at the memory root; the new layout
|
|
300
|
+
* tucks each into `group/<g>/{vp,feature,topic}/...`. Per user directive
|
|
301
|
+
* "硬切,老的就不要了" — we do NOT migrate per-record, we just move the
|
|
302
|
+
* top-level dirs to `<root>/.legacy/<kind>/` once. They are never read
|
|
303
|
+
* again; this is forensics-only.
|
|
304
|
+
*
|
|
305
|
+
* Idempotent: a second invocation is a no-op when no legacy dirs remain at
|
|
306
|
+
* the root. If `.legacy/<kind>/` already exists, the new move is suffixed
|
|
307
|
+
* with a timestamp so re-attempts after a partial first run don't clobber.
|
|
308
|
+
*
|
|
309
|
+
* @param {string} root memory root (typically <yeaftDir>/memory)
|
|
310
|
+
* @returns {{moved: string[]}}
|
|
311
|
+
*/
|
|
312
|
+
export function archiveLegacyScopes(root) {
|
|
313
|
+
const moved = [];
|
|
314
|
+
if (!root || !existsSync(root)) return { moved };
|
|
315
|
+
const legacyRoot = join(root, '.legacy');
|
|
316
|
+
for (const kind of ['vp', 'feature', 'topic']) {
|
|
317
|
+
const src = join(root, kind);
|
|
318
|
+
if (!existsSync(src)) continue;
|
|
319
|
+
try {
|
|
320
|
+
mkdirSync(legacyRoot, { recursive: true });
|
|
321
|
+
let dst = join(legacyRoot, kind);
|
|
322
|
+
if (existsSync(dst)) {
|
|
323
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
324
|
+
dst = `${dst}.${ts}`;
|
|
325
|
+
}
|
|
326
|
+
// eslint-disable-next-line global-require
|
|
327
|
+
renameSync(src, dst);
|
|
328
|
+
moved.push(kind);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
console.warn(`[seed-backfill] archiveLegacyScopes(${kind}) failed:`, err?.message || err);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return { moved };
|
|
334
|
+
}
|
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|
|
|
54
|
+
const SCOPE_RE = /^(user|group\/[\w-]+(?:\/(?:user|vp\/[\w-]+|feature\/[\w-]+|topic\/[\w-]+(?:\/[\w-]+)?))?)$/;
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* Compute a stable id from segment content. Same body + scope + kind →
|
package/yeaft/memory/store-v2.js
CHANGED
|
@@ -54,16 +54,24 @@ import { homedir } from 'os';
|
|
|
54
54
|
/** Default memory root. Tests override via `opts.root`. */
|
|
55
55
|
export const DEFAULT_MEMORY_ROOT = join(homedir(), '.yeaft', 'memory');
|
|
56
56
|
|
|
57
|
-
/** Scope kinds recognised by v2. */
|
|
58
|
-
export const SCOPE_KINDS = Object.freeze([
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
/** Scope kinds recognised by v2 (group-isolated layout). */
|
|
58
|
+
export const SCOPE_KINDS = Object.freeze([
|
|
59
|
+
'user',
|
|
60
|
+
'group',
|
|
61
|
+
'group-user',
|
|
62
|
+
'group-vp',
|
|
63
|
+
'group-feature',
|
|
64
|
+
'group-topic',
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
/** @typedef {'user'|'group'|'group-user'|'group-vp'|'group-feature'|'group-topic'} ScopeKind */
|
|
61
68
|
|
|
62
69
|
/**
|
|
63
70
|
* @typedef {Object} Scope
|
|
64
71
|
* @property {ScopeKind} kind
|
|
65
|
-
* @property {string} [id]
|
|
66
|
-
* @property {string
|
|
72
|
+
* @property {string} [id] — required for group; for group-vp / group-feature the per-kind id
|
|
73
|
+
* @property {string} [groupId] — required for every group-* kind
|
|
74
|
+
* @property {string[]} [path] — required for group-topic; 1–2 segments
|
|
67
75
|
*/
|
|
68
76
|
|
|
69
77
|
/**
|
|
@@ -81,25 +89,38 @@ export function scopeDir(scope) {
|
|
|
81
89
|
switch (scope.kind) {
|
|
82
90
|
case 'user':
|
|
83
91
|
return 'user';
|
|
84
|
-
case 'vp':
|
|
85
|
-
if (!scope.id) throw new Error('scopeDir: vp scope requires id');
|
|
86
|
-
assertSafeSegment(scope.id, 'vp.id');
|
|
87
|
-
return `vp/${scope.id}`;
|
|
88
92
|
case 'group':
|
|
89
93
|
if (!scope.id) throw new Error('scopeDir: group scope requires id');
|
|
90
94
|
assertSafeSegment(scope.id, 'group.id');
|
|
91
95
|
return `group/${scope.id}`;
|
|
92
|
-
case '
|
|
93
|
-
if (!scope.
|
|
94
|
-
assertSafeSegment(scope.
|
|
95
|
-
return `
|
|
96
|
-
|
|
96
|
+
case 'group-user': {
|
|
97
|
+
if (!scope.groupId) throw new Error('scopeDir: group-user scope requires groupId');
|
|
98
|
+
assertSafeSegment(scope.groupId, 'group-user.groupId');
|
|
99
|
+
return `group/${scope.groupId}/user`;
|
|
100
|
+
}
|
|
101
|
+
case 'group-vp': {
|
|
102
|
+
if (!scope.groupId) throw new Error('scopeDir: group-vp scope requires groupId');
|
|
103
|
+
if (!scope.id) throw new Error('scopeDir: group-vp scope requires id');
|
|
104
|
+
assertSafeSegment(scope.groupId, 'group-vp.groupId');
|
|
105
|
+
assertSafeSegment(scope.id, 'group-vp.id');
|
|
106
|
+
return `group/${scope.groupId}/vp/${scope.id}`;
|
|
107
|
+
}
|
|
108
|
+
case 'group-feature': {
|
|
109
|
+
if (!scope.groupId) throw new Error('scopeDir: group-feature scope requires groupId');
|
|
110
|
+
if (!scope.id) throw new Error('scopeDir: group-feature scope requires id');
|
|
111
|
+
assertSafeSegment(scope.groupId, 'group-feature.groupId');
|
|
112
|
+
assertSafeSegment(scope.id, 'group-feature.id');
|
|
113
|
+
return `group/${scope.groupId}/feature/${scope.id}`;
|
|
114
|
+
}
|
|
115
|
+
case 'group-topic': {
|
|
116
|
+
if (!scope.groupId) throw new Error('scopeDir: group-topic scope requires groupId');
|
|
117
|
+
assertSafeSegment(scope.groupId, 'group-topic.groupId');
|
|
97
118
|
const segs = Array.isArray(scope.path) ? scope.path : [];
|
|
98
119
|
if (segs.length === 0 || segs.length > 2) {
|
|
99
|
-
throw new Error('scopeDir: topic.path must have 1 or 2 segments');
|
|
120
|
+
throw new Error('scopeDir: group-topic.path must have 1 or 2 segments');
|
|
100
121
|
}
|
|
101
|
-
for (const s of segs) assertSafeSegment(s, 'topic.path');
|
|
102
|
-
return `topic/${segs.join('/')}`;
|
|
122
|
+
for (const s of segs) assertSafeSegment(s, 'group-topic.path');
|
|
123
|
+
return `group/${scope.groupId}/topic/${segs.join('/')}`;
|
|
103
124
|
}
|
|
104
125
|
default:
|
|
105
126
|
throw new Error(`scopeDir: unknown kind ${JSON.stringify(scope.kind)}`);
|
|
@@ -140,7 +161,8 @@ function assertSafeSegment(s, ctx) {
|
|
|
140
161
|
* @returns {boolean}
|
|
141
162
|
*/
|
|
142
163
|
export function isValidTopic(scope) {
|
|
143
|
-
if (!scope || scope.kind !== 'topic') return false;
|
|
164
|
+
if (!scope || scope.kind !== 'group-topic') return false;
|
|
165
|
+
if (!scope.groupId || typeof scope.groupId !== 'string') return false;
|
|
144
166
|
if (!Array.isArray(scope.path)) return false;
|
|
145
167
|
if (scope.path.length < 1 || scope.path.length > 2) return false;
|
|
146
168
|
for (const s of scope.path) {
|
|
@@ -155,7 +177,9 @@ export function isValidTopic(scope) {
|
|
|
155
177
|
// ─── ACL ───────────────────────────────────────────────────────
|
|
156
178
|
|
|
157
179
|
/**
|
|
158
|
-
* The single ACL: `vp/<other>` is foreign when `currentVpId` is given.
|
|
180
|
+
* The single ACL: `group/<g>/vp/<other>` is foreign when `currentVpId` is given.
|
|
181
|
+
* Across groups, every `group/<g>/vp/...` path is foreign by construction
|
|
182
|
+
* (the calling VP only runs inside its own group dir).
|
|
159
183
|
*
|
|
160
184
|
* @param {string} relPath
|
|
161
185
|
* @param {string} currentVpId
|
|
@@ -163,7 +187,7 @@ export function isValidTopic(scope) {
|
|
|
163
187
|
*/
|
|
164
188
|
export function isVpForeign(relPath, currentVpId) {
|
|
165
189
|
if (!relPath || !currentVpId) return false;
|
|
166
|
-
const m = /^vp\/([^/]+)(?:\/|$)/.exec(relPath);
|
|
190
|
+
const m = /^group\/[^/]+\/vp\/([^/]+)(?:\/|$)/.exec(relPath);
|
|
167
191
|
if (!m) return false;
|
|
168
192
|
return m[1] !== currentVpId;
|
|
169
193
|
}
|
|
@@ -403,18 +427,17 @@ export async function ensureScope(scope, opts = {}) {
|
|
|
403
427
|
|
|
404
428
|
/**
|
|
405
429
|
* Enumerate all scopes present on disk. Returns Scope shapes that round-trip
|
|
406
|
-
* back through `scopeDir`. Used by Triage to list candidate
|
|
407
|
-
* scopes for a group's diff.
|
|
430
|
+
* back through `scopeDir`. Used by Triage to list candidate scopes.
|
|
408
431
|
*
|
|
409
432
|
* Walks shallowly:
|
|
410
|
-
* user/
|
|
411
|
-
*
|
|
412
|
-
* group/<
|
|
413
|
-
*
|
|
414
|
-
*
|
|
433
|
+
* user/ → { kind: 'user' }
|
|
434
|
+
* group/<g>/ → { kind: 'group', id: g }
|
|
435
|
+
* group/<g>/user/ → { kind: 'group-user', groupId: g }
|
|
436
|
+
* group/<g>/vp/<v>/ → { kind: 'group-vp', groupId: g, id: v }
|
|
437
|
+
* group/<g>/feature/<f>/ → { kind: 'group-feature', groupId: g, id: f }
|
|
438
|
+
* group/<g>/topic/<l1>[/<l2>]/ → { kind: 'group-topic', groupId: g, path: [...] }
|
|
415
439
|
*
|
|
416
|
-
* Skips
|
|
417
|
-
* validation (e.g. accidental `.tmp.*` files at scope root, dotfiles).
|
|
440
|
+
* Skips `.legacy/` and any dotfile / unsafe segment.
|
|
418
441
|
*
|
|
419
442
|
* @param {{ root?: string }} [opts]
|
|
420
443
|
* @returns {Promise<Scope[]>}
|
|
@@ -427,53 +450,77 @@ export async function listScopes(opts = {}) {
|
|
|
427
450
|
// user/
|
|
428
451
|
if (existsSync(join(root, 'user'))) out.push({ kind: 'user' });
|
|
429
452
|
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
453
|
+
// group/<g>/...
|
|
454
|
+
const groupRoot = join(root, 'group');
|
|
455
|
+
let groups;
|
|
456
|
+
try { groups = await fsp.readdir(groupRoot, { withFileTypes: true }); }
|
|
457
|
+
catch (err) {
|
|
458
|
+
if (err && err.code === 'ENOENT') return out;
|
|
459
|
+
throw err;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
for (const gent of groups) {
|
|
463
|
+
if (!gent.isDirectory()) continue;
|
|
464
|
+
if (gent.name.startsWith('.')) continue;
|
|
465
|
+
if (!isSafeId(gent.name)) continue;
|
|
466
|
+
const g = gent.name;
|
|
467
|
+
out.push({ kind: 'group', id: g });
|
|
468
|
+
const gAbs = join(groupRoot, g);
|
|
469
|
+
|
|
470
|
+
// group/<g>/user/
|
|
471
|
+
if (existsSync(join(gAbs, 'user'))) {
|
|
472
|
+
out.push({ kind: 'group-user', groupId: g });
|
|
438
473
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
474
|
+
|
|
475
|
+
// group/<g>/vp/<v>/ and group/<g>/feature/<f>/
|
|
476
|
+
for (const kind of ['vp', 'feature']) {
|
|
477
|
+
const dir = join(gAbs, kind);
|
|
478
|
+
let names;
|
|
479
|
+
try { names = await fsp.readdir(dir, { withFileTypes: true }); }
|
|
480
|
+
catch (err) {
|
|
481
|
+
if (err && err.code === 'ENOENT') continue;
|
|
482
|
+
throw err;
|
|
483
|
+
}
|
|
484
|
+
for (const ent of names) {
|
|
485
|
+
if (!ent.isDirectory()) continue;
|
|
486
|
+
if (!isSafeId(ent.name)) continue;
|
|
487
|
+
out.push({
|
|
488
|
+
kind: kind === 'vp' ? 'group-vp' : 'group-feature',
|
|
489
|
+
groupId: g,
|
|
490
|
+
id: ent.name,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
444
493
|
}
|
|
445
|
-
}
|
|
446
494
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
for (const l1ent of l1s) {
|
|
456
|
-
if (!l1ent.isDirectory()) continue;
|
|
457
|
-
if (!isSafeId(l1ent.name)) continue;
|
|
458
|
-
const l1 = l1ent.name;
|
|
459
|
-
// Read l2 entries; if l1 itself contains memory.md, treat as 1-level topic
|
|
460
|
-
const l1abs = join(topicDir, l1);
|
|
461
|
-
let l2s;
|
|
462
|
-
try { l2s = await fsp.readdir(l1abs, { withFileTypes: true }); }
|
|
463
|
-
catch { l2s = []; }
|
|
464
|
-
let hasL2 = false;
|
|
465
|
-
for (const l2ent of l2s) {
|
|
466
|
-
if (!l2ent.isDirectory()) continue;
|
|
467
|
-
if (!isSafeId(l2ent.name)) continue;
|
|
468
|
-
out.push({ kind: 'topic', path: [l1, l2ent.name] });
|
|
469
|
-
hasL2 = true;
|
|
495
|
+
// group/<g>/topic/<l1>/[<l2>/]
|
|
496
|
+
const topicDir = join(gAbs, 'topic');
|
|
497
|
+
let l1s;
|
|
498
|
+
try { l1s = await fsp.readdir(topicDir, { withFileTypes: true }); }
|
|
499
|
+
catch (err) {
|
|
500
|
+
if (err && err.code === 'ENOENT') l1s = [];
|
|
501
|
+
else throw err;
|
|
470
502
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
503
|
+
for (const l1ent of l1s) {
|
|
504
|
+
if (!l1ent.isDirectory()) continue;
|
|
505
|
+
if (!isSafeId(l1ent.name)) continue;
|
|
506
|
+
const l1 = l1ent.name;
|
|
507
|
+
const l1abs = join(topicDir, l1);
|
|
508
|
+
let l2s;
|
|
509
|
+
try { l2s = await fsp.readdir(l1abs, { withFileTypes: true }); }
|
|
510
|
+
catch { l2s = []; }
|
|
511
|
+
let hasL2 = false;
|
|
512
|
+
for (const l2ent of l2s) {
|
|
513
|
+
if (!l2ent.isDirectory()) continue;
|
|
514
|
+
if (!isSafeId(l2ent.name)) continue;
|
|
515
|
+
out.push({ kind: 'group-topic', groupId: g, path: [l1, l2ent.name] });
|
|
516
|
+
hasL2 = true;
|
|
517
|
+
}
|
|
518
|
+
if (!hasL2) {
|
|
519
|
+
const hasMemory = existsSync(join(l1abs, 'memory.md'));
|
|
520
|
+
const hasSummary = existsSync(join(l1abs, 'summary.md'));
|
|
521
|
+
if (hasMemory || hasSummary) {
|
|
522
|
+
out.push({ kind: 'group-topic', groupId: g, path: [l1] });
|
|
523
|
+
}
|
|
477
524
|
}
|
|
478
525
|
}
|
|
479
526
|
}
|
package/yeaft/session.js
CHANGED
|
@@ -45,7 +45,7 @@ import { ToolUsageStats } from './stats/tool-usage.js';
|
|
|
45
45
|
import { ensureDefaultGroupIfEmpty } from './groups/group-crud.js';
|
|
46
46
|
import { seedDefaultVps } from './vp/seed-defaults.js';
|
|
47
47
|
import { topUpDefaultVps } from './vp/seed-topup.js';
|
|
48
|
-
import { runSummaryBackfill } from './memory/seed-backfill.js';
|
|
48
|
+
import { runSummaryBackfill, archiveLegacyScopes } from './memory/seed-backfill.js';
|
|
49
49
|
import { createV2DreamScheduler, bootInitEmptyGroups, bootCatchUpStaleDream } from './dream-v2/session-wiring.js';
|
|
50
50
|
import { openSegmentIndex } from './memory/index-db.js';
|
|
51
51
|
import { syncAll as syncSegmentIndex } from './memory/segment-sync.js';
|
|
@@ -235,6 +235,16 @@ export async function loadSession(options = {}) {
|
|
|
235
235
|
const indexPath = join(yeaftDir, 'memory', 'index.db');
|
|
236
236
|
memoryIndex = openSegmentIndex(indexPath);
|
|
237
237
|
const memoryRoot = join(yeaftDir, 'memory');
|
|
238
|
+
// One-shot migration to the group-isolated memory layout: move any
|
|
239
|
+
// remaining top-level vp/ feature/ topic/ dirs into .legacy/ before
|
|
240
|
+
// we open the FTS index and re-sync from disk.
|
|
241
|
+
try {
|
|
242
|
+
archiveLegacyScopes(memoryRoot);
|
|
243
|
+
} catch (archiveErr) {
|
|
244
|
+
if (config.debug) {
|
|
245
|
+
console.warn(`[Yeaft] legacy scope archive warning: ${archiveErr?.message || archiveErr}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
238
248
|
try {
|
|
239
249
|
syncSegmentIndex(memoryRoot, memoryIndex);
|
|
240
250
|
} catch (syncErr) {
|
package/yeaft/vp/vp-crud.js
CHANGED
|
@@ -23,7 +23,6 @@ import { join } from 'path';
|
|
|
23
23
|
import { homedir } from 'os';
|
|
24
24
|
import { validateVpId } from '../groups/ids.js';
|
|
25
25
|
import { DEFAULT_VP_LIB_DIR, parseRoleMd } from './vp-store.js';
|
|
26
|
-
import { seedSummaryIfMissingSync, removeScopeDirSync } from '../memory/store-v2.js';
|
|
27
26
|
import { VP_STUB_MARKER } from '../memory/seed-backfill.js';
|
|
28
27
|
import { STOCK_VP_IDS } from './stock-ids.js';
|
|
29
28
|
|
|
@@ -178,22 +177,9 @@ export function createVp(payload, options = {}) {
|
|
|
178
177
|
mkdirSync(join(dir, 'memory'), { recursive: true });
|
|
179
178
|
writeFileSync(vpRolePathFor(libDir, vpId), buildRoleMd({ ...payload, vpId }), 'utf-8');
|
|
180
179
|
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
// requires a non-empty diff stream — i.e. several turns of activity).
|
|
185
|
-
// We only seed when the file is missing/empty: this is safe to re-run and
|
|
186
|
-
// never clobbers Dream-v2 writes. Failures are best-effort: a memory-root
|
|
187
|
-
// permission failure must NOT break VP creation.
|
|
188
|
-
try {
|
|
189
|
-
seedSummaryIfMissingSync(
|
|
190
|
-
{ kind: 'vp', id: vpId },
|
|
191
|
-
buildVpSeedSummary({ ...payload, vpId }),
|
|
192
|
-
{ root: memoryRoot },
|
|
193
|
-
);
|
|
194
|
-
} catch (err) {
|
|
195
|
-
console.warn(`[vp-crud] failed to seed summary.md for ${vpId}:`, err?.message || err);
|
|
196
|
-
}
|
|
180
|
+
// Note: VP memory is now per-group (group/<g>/vp/<vpId>/...) — no global
|
|
181
|
+
// VP scope to seed at create time. Per-group summaries spring into being
|
|
182
|
+
// when dream first writes for that VP inside a group.
|
|
197
183
|
|
|
198
184
|
return { vpId, dir };
|
|
199
185
|
}
|
|
@@ -258,13 +244,10 @@ export function deleteVp(vpId, options = {}) {
|
|
|
258
244
|
throw new VpCrudError('not_found', vpId);
|
|
259
245
|
}
|
|
260
246
|
rmSync(dir, { recursive: true, force: true });
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
} catch (err) {
|
|
266
|
-
console.warn(`[vp-crud] failed to remove memory dir for ${vpId}:`, err?.message || err);
|
|
267
|
-
}
|
|
247
|
+
// VP memory is per-group now (group/<g>/vp/<id>/...). We deliberately do
|
|
248
|
+
// NOT cascade-delete across every group dir on disk — that would couple
|
|
249
|
+
// VP CRUD to the group registry. Stale per-group VP scopes get pruned by
|
|
250
|
+
// dream's natural rewrite cycle, or by group deletion.
|
|
268
251
|
return { vpId };
|
|
269
252
|
}
|
|
270
253
|
|