@yeaft/webchat-agent 1.0.290 → 1.0.293
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 +5 -1
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +197 -164
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/engine.js +64 -17
- package/yeaft/memory/preflow.js +8 -2
- package/yeaft/projects/store.js +152 -0
- package/yeaft/sessions/pre-flow.js +5 -0
- package/yeaft/web-bridge.js +115 -2
|
Binary file
|
package/package.json
CHANGED
package/yeaft/engine.js
CHANGED
|
@@ -84,6 +84,8 @@ const AMS_ADJUST_TIMEOUT_MS = 30_000;
|
|
|
84
84
|
/** Maximum silence while a visible turn waits for a result-producing task. */
|
|
85
85
|
const DEFAULT_ASYNC_TASK_WAIT_TIMEOUT_MS = 120_000;
|
|
86
86
|
|
|
87
|
+
const DEFAULT_MEMORY_RECALL_LIMIT = 8;
|
|
88
|
+
|
|
87
89
|
// ─── LLM retry policy defaults ──────────────────────────────────
|
|
88
90
|
// Hard-coded floor / ceiling for retry behaviour. The engine reads the
|
|
89
91
|
// effective policy from `config.llmRetry` so users can dial these via
|
|
@@ -442,6 +444,47 @@ function isZhRuntimeLanguage(language) {
|
|
|
442
444
|
return String(language || '').toLowerCase().startsWith('zh');
|
|
443
445
|
}
|
|
444
446
|
|
|
447
|
+
function resolveMemoryRecallLimit(config) {
|
|
448
|
+
const raw = config?.memoryRecallLimit ?? config?.dreamMemoryRecallLimit;
|
|
449
|
+
if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_MEMORY_RECALL_LIMIT;
|
|
450
|
+
return Math.max(1, Math.floor(raw));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function loadedMemoryDebugEntries(snapshot) {
|
|
454
|
+
const snap = snapshot || {};
|
|
455
|
+
return [
|
|
456
|
+
...loadedResidentDebugEntries(snap.resident || []),
|
|
457
|
+
...loadedSegmentDebugEntries(snap.recent || [], 'recent'),
|
|
458
|
+
...loadedSegmentDebugEntries(snap.onDemand || [], 'onDemand'),
|
|
459
|
+
];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function loadedResidentDebugEntries(entries) {
|
|
463
|
+
return (entries || []).map((entry, index) => ({
|
|
464
|
+
id: `resident:${entry.scope || index}`,
|
|
465
|
+
layer: 'resident',
|
|
466
|
+
scope: entry.scope || null,
|
|
467
|
+
label: memoryScopeLabel(entry.scope || ''),
|
|
468
|
+
kind: 'summary',
|
|
469
|
+
score: null,
|
|
470
|
+
tags: [],
|
|
471
|
+
body: entry.summary || '',
|
|
472
|
+
})).filter(entry => entry.body);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function loadedSegmentDebugEntries(segments, layer) {
|
|
476
|
+
return (segments || []).map((seg, index) => ({
|
|
477
|
+
id: seg.id || `${layer}:${index}`,
|
|
478
|
+
layer,
|
|
479
|
+
scope: seg.scope || null,
|
|
480
|
+
label: memoryScopeLabel(seg.scope || ''),
|
|
481
|
+
kind: seg.kind || null,
|
|
482
|
+
score: typeof seg.score === 'number' ? seg.score : null,
|
|
483
|
+
tags: Array.isArray(seg.tags) ? seg.tags : [],
|
|
484
|
+
body: seg.body || '',
|
|
485
|
+
})).filter(entry => entry.body);
|
|
486
|
+
}
|
|
487
|
+
|
|
445
488
|
export class Engine {
|
|
446
489
|
/** @type {import('./llm/adapter.js').LLMAdapter} */
|
|
447
490
|
#adapter;
|
|
@@ -906,6 +949,7 @@ export class Engine {
|
|
|
906
949
|
* ownVpId: string|null,
|
|
907
950
|
* scopes: string[],
|
|
908
951
|
* snapshotBlock: string,
|
|
952
|
+
* snapshot: import('./memory/ams.js').AmsSnapshot,
|
|
909
953
|
* residentEntries: Array<{scope:string, summary:string}>,
|
|
910
954
|
* } | null}
|
|
911
955
|
*/
|
|
@@ -938,14 +982,15 @@ export class Engine {
|
|
|
938
982
|
ams.setOnDemand(segs);
|
|
939
983
|
|
|
940
984
|
// (c) Snapshot — render the AMS layers as a single prompt block.
|
|
941
|
-
const
|
|
985
|
+
const snapshot = ams.snapshot({ userMsg: args.userMsg || '' });
|
|
986
|
+
const snapshotBlock = this.#renderAmsSnapshot(snapshot, this.#config.language || 'en');
|
|
942
987
|
|
|
943
988
|
const scopes = buildRelevantScopes({
|
|
944
989
|
sessionId: args.sessionId,
|
|
945
990
|
vpId: ownVpId,
|
|
946
991
|
});
|
|
947
992
|
|
|
948
|
-
return { ams, sessionKey, ownVpId, scopes, snapshotBlock, residentEntries };
|
|
993
|
+
return { ams, sessionKey, ownVpId, scopes, snapshotBlock, snapshot, residentEntries };
|
|
949
994
|
}
|
|
950
995
|
|
|
951
996
|
/**
|
|
@@ -953,13 +998,11 @@ export class Engine {
|
|
|
953
998
|
* injection. Mirrors the heading style of the existing memory blocks
|
|
954
999
|
* so the LLM sees a consistent layout.
|
|
955
1000
|
*
|
|
956
|
-
* @param {import('./memory/ams.js').
|
|
1001
|
+
* @param {import('./memory/ams.js').AmsSnapshot} snap
|
|
957
1002
|
* @param {string} [language]
|
|
958
|
-
* @param {string} [userMsg]
|
|
959
1003
|
* @returns {string}
|
|
960
1004
|
*/
|
|
961
|
-
#renderAmsSnapshot(
|
|
962
|
-
const snap = ams.snapshot({ userMsg });
|
|
1005
|
+
#renderAmsSnapshot(snap, language = 'en') {
|
|
963
1006
|
if (!snap) return '';
|
|
964
1007
|
const parts = [];
|
|
965
1008
|
if (snap.resident.length === 0 && snap.recent.length === 0 && snap.onDemand.length === 0) {
|
|
@@ -1342,7 +1385,7 @@ export class Engine {
|
|
|
1342
1385
|
* @returns {Promise<{ profile: string, entries: object[], formatted: string }|null>}
|
|
1343
1386
|
*/
|
|
1344
1387
|
async #recallMemory(prompt, ctx = {}) {
|
|
1345
|
-
const memory = { profile: '', entries: [], formatted: '' };
|
|
1388
|
+
const memory = { profile: '', entries: [], formatted: '', meta: {} };
|
|
1346
1389
|
if (!this.#memoryIndex) return memory;
|
|
1347
1390
|
try {
|
|
1348
1391
|
const result = runMemoryPreflow(this.#memoryIndex, {
|
|
@@ -1351,11 +1394,13 @@ export class Engine {
|
|
|
1351
1394
|
chatId: ctx.chatId || this.#chatId,
|
|
1352
1395
|
vpId: ctx.vpId,
|
|
1353
1396
|
extraScopes: ctx.extraScopes,
|
|
1397
|
+
pickLimit: resolveMemoryRecallLimit(this.#config),
|
|
1354
1398
|
fallbackOnEmpty: true,
|
|
1355
1399
|
});
|
|
1356
1400
|
memory.profile = result.profile || '';
|
|
1357
1401
|
memory.entries = result.entries || [];
|
|
1358
1402
|
memory.formatted = result.formatted || '';
|
|
1403
|
+
memory.meta = result.meta || {};
|
|
1359
1404
|
} catch {
|
|
1360
1405
|
// Fail soft — empty injection.
|
|
1361
1406
|
}
|
|
@@ -2029,6 +2074,7 @@ export class Engine {
|
|
|
2029
2074
|
if (amsContext && amsContext.snapshotBlock) {
|
|
2030
2075
|
memoryInjection = amsContext.snapshotBlock;
|
|
2031
2076
|
}
|
|
2077
|
+
const loadedMemoryForDebug = loadedMemoryDebugEntries(amsContext?.snapshot);
|
|
2032
2078
|
|
|
2033
2079
|
// Diagnostic payload for the Dream debug panel. The full AMS Resident
|
|
2034
2080
|
// layer can include user and per-VP summaries, but the browser-facing
|
|
@@ -2282,19 +2328,20 @@ export class Engine {
|
|
|
2282
2328
|
yield { type: 'skill_error', turnId: queryTurnId, skillName: explicitSkillName, message: skillResolutionError };
|
|
2283
2329
|
}
|
|
2284
2330
|
|
|
2285
|
-
// Surface memory
|
|
2286
|
-
//
|
|
2287
|
-
//
|
|
2288
|
-
|
|
2289
|
-
if (recallResult && Array.isArray(recallResult.entries) && recallResult.entries.length > 0) {
|
|
2331
|
+
// Surface the exact memory that entered the prompt. This must be based on
|
|
2332
|
+
// the AMS snapshot, not raw FTS candidates, otherwise debug can claim memory
|
|
2333
|
+
// was loaded even when prompt cleanup, dedupe, or token budget dropped it.
|
|
2334
|
+
if (loadedMemoryForDebug.length > 0) {
|
|
2290
2335
|
yield {
|
|
2291
2336
|
type: 'memory_used',
|
|
2292
2337
|
turnId: queryTurnId,
|
|
2293
|
-
loaded:
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2338
|
+
loaded: loadedMemoryForDebug,
|
|
2339
|
+
meta: {
|
|
2340
|
+
recallLimit: resolveMemoryRecallLimit(this.#config),
|
|
2341
|
+
recallCandidates: Number.isFinite(recallResult?.meta?.hitCount)
|
|
2342
|
+
? recallResult.meta.hitCount
|
|
2343
|
+
: (recallResult && Array.isArray(recallResult.entries) ? recallResult.entries.length : 0),
|
|
2344
|
+
},
|
|
2298
2345
|
};
|
|
2299
2346
|
}
|
|
2300
2347
|
|
package/yeaft/memory/preflow.js
CHANGED
|
@@ -19,6 +19,8 @@ import { extractKeywords } from './keywords.js';
|
|
|
19
19
|
import { approxTokens } from './budget.js';
|
|
20
20
|
import { isVpForeign } from './store.js';
|
|
21
21
|
|
|
22
|
+
export const DEFAULT_PICK_LIMIT = 8;
|
|
23
|
+
|
|
22
24
|
/**
|
|
23
25
|
* @typedef {object} PreflowOptions
|
|
24
26
|
* @property {string} userMsg
|
|
@@ -27,6 +29,7 @@ import { isVpForeign } from './store.js';
|
|
|
27
29
|
* @property {string[]} [currentTags] tags from the current group/feature context
|
|
28
30
|
* @property {number} [topK] max FTS rows to fetch (default 50)
|
|
29
31
|
* @property {number} [budgetTokens] onDemand budget (caller-supplied)
|
|
32
|
+
* @property {number} [pickLimit] max picked segments (default 8)
|
|
30
33
|
*/
|
|
31
34
|
|
|
32
35
|
/**
|
|
@@ -54,6 +57,8 @@ export function runPreflow(index, opts) {
|
|
|
54
57
|
const topK = Number.isFinite(opts.topK) && opts.topK > 0 ? opts.topK : 50;
|
|
55
58
|
const budgetTokens = Number.isFinite(opts.budgetTokens) && opts.budgetTokens > 0
|
|
56
59
|
? opts.budgetTokens : Infinity;
|
|
60
|
+
const pickLimit = Number.isFinite(opts.pickLimit) && opts.pickLimit > 0
|
|
61
|
+
? Math.floor(opts.pickLimit) : DEFAULT_PICK_LIMIT;
|
|
57
62
|
|
|
58
63
|
const keywords = extractKeywords(userMsg);
|
|
59
64
|
if (keywords.length === 0) {
|
|
@@ -80,7 +85,7 @@ export function runPreflow(index, opts) {
|
|
|
80
85
|
let dropped = 0;
|
|
81
86
|
for (const h of reranked) {
|
|
82
87
|
const tk = approxTokens(h.body);
|
|
83
|
-
if (cost + tk <= budgetTokens) {
|
|
88
|
+
if (picked.length < pickLimit && cost + tk <= budgetTokens) {
|
|
84
89
|
picked.push(toSegment(h));
|
|
85
90
|
cost += tk;
|
|
86
91
|
} else {
|
|
@@ -152,7 +157,7 @@ export function rerank(hits, ctx) {
|
|
|
152
157
|
return { ...h, _score: score };
|
|
153
158
|
})
|
|
154
159
|
.sort((a, b) => a._score - b._score)
|
|
155
|
-
.map(({ _score, ...rest }) => rest);
|
|
160
|
+
.map(({ _score, ...rest }) => ({ ...rest, score: _score }));
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
function toSegment(h) {
|
|
@@ -163,6 +168,7 @@ function toSegment(h) {
|
|
|
163
168
|
tags: h.tags,
|
|
164
169
|
sourceMessages: h.sourceMessages,
|
|
165
170
|
body: h.body,
|
|
171
|
+
score: typeof h.score === 'number' ? h.score : (typeof h.rank === 'number' ? h.rank : undefined),
|
|
166
172
|
createdAt: h.createdAt,
|
|
167
173
|
updatedAt: h.updatedAt,
|
|
168
174
|
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { writeAtomic } from '../storage/atomic.js';
|
|
5
|
+
|
|
6
|
+
const PROJECTS_FILE = 'projects.json';
|
|
7
|
+
const PROJECTS_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
export class ProjectStoreError extends Error {
|
|
10
|
+
constructor(code, message) {
|
|
11
|
+
super(message || code);
|
|
12
|
+
this.name = 'ProjectStoreError';
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function projectsPath(yeaftDir) {
|
|
18
|
+
return join(yeaftDir, PROJECTS_FILE);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeSessionIds(values) {
|
|
22
|
+
const out = [];
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
for (const value of Array.isArray(values) ? values : []) {
|
|
25
|
+
const id = typeof value === 'string' ? value.trim() : '';
|
|
26
|
+
if (!id || seen.has(id)) continue;
|
|
27
|
+
seen.add(id);
|
|
28
|
+
out.push(id);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeProject(row, index = 0) {
|
|
34
|
+
if (!row || typeof row !== 'object' || typeof row.id !== 'string' || !row.id) return null;
|
|
35
|
+
const name = typeof row.name === 'string' ? row.name.trim() : '';
|
|
36
|
+
if (!name) return null;
|
|
37
|
+
return {
|
|
38
|
+
id: row.id,
|
|
39
|
+
name,
|
|
40
|
+
sessionIds: normalizeSessionIds(row.sessionIds),
|
|
41
|
+
sortOrder: Number.isFinite(row.sortOrder) ? row.sortOrder : index,
|
|
42
|
+
createdAt: typeof row.createdAt === 'string' ? row.createdAt : '',
|
|
43
|
+
updatedAt: typeof row.updatedAt === 'string' ? row.updatedAt : '',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function loadProjects(yeaftDir) {
|
|
48
|
+
if (!yeaftDir || !existsSync(projectsPath(yeaftDir))) return [];
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(readFileSync(projectsPath(yeaftDir), 'utf8'));
|
|
51
|
+
const rows = Array.isArray(parsed?.projects) ? parsed.projects : [];
|
|
52
|
+
return rows.map(normalizeProject).filter(Boolean).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
53
|
+
} catch {
|
|
54
|
+
const path = projectsPath(yeaftDir);
|
|
55
|
+
try { renameSync(path, `${path}.corrupt-${Date.now()}`); } catch { /* keep the unreadable file if quarantine fails */ }
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function saveProjects(yeaftDir, projects) {
|
|
61
|
+
if (!yeaftDir) throw new ProjectStoreError('missing_root', 'Yeaft directory is required');
|
|
62
|
+
mkdirSync(yeaftDir, { recursive: true });
|
|
63
|
+
const now = new Date().toISOString();
|
|
64
|
+
const normalized = projects.map(normalizeProject).filter(Boolean).map((project, index) => ({
|
|
65
|
+
...project,
|
|
66
|
+
sortOrder: index,
|
|
67
|
+
updatedAt: project.updatedAt || now,
|
|
68
|
+
}));
|
|
69
|
+
writeAtomic(projectsPath(yeaftDir), `${JSON.stringify({
|
|
70
|
+
version: PROJECTS_VERSION,
|
|
71
|
+
updatedAt: now,
|
|
72
|
+
projects: normalized,
|
|
73
|
+
}, null, 2)}\n`);
|
|
74
|
+
return normalized;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function requireName(value) {
|
|
78
|
+
const name = typeof value === 'string' ? value.trim() : '';
|
|
79
|
+
if (!name) throw new ProjectStoreError('invalid_name', 'Project name is required');
|
|
80
|
+
return name.slice(0, 120);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function requireProject(projects, projectId) {
|
|
84
|
+
const project = projects.find(row => row.id === projectId);
|
|
85
|
+
if (!project) throw new ProjectStoreError('not_found', 'Project not found');
|
|
86
|
+
return project;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createProject(yeaftDir, name) {
|
|
90
|
+
const projects = loadProjects(yeaftDir);
|
|
91
|
+
const now = new Date().toISOString();
|
|
92
|
+
const project = {
|
|
93
|
+
id: `project-${randomUUID().slice(0, 8)}`,
|
|
94
|
+
name: requireName(name),
|
|
95
|
+
sessionIds: [],
|
|
96
|
+
sortOrder: projects.length,
|
|
97
|
+
createdAt: now,
|
|
98
|
+
updatedAt: now,
|
|
99
|
+
};
|
|
100
|
+
saveProjects(yeaftDir, [...projects, project]);
|
|
101
|
+
return project;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function renameProject(yeaftDir, projectId, name) {
|
|
105
|
+
const projects = loadProjects(yeaftDir);
|
|
106
|
+
const project = requireProject(projects, projectId);
|
|
107
|
+
project.name = requireName(name);
|
|
108
|
+
project.updatedAt = new Date().toISOString();
|
|
109
|
+
saveProjects(yeaftDir, projects);
|
|
110
|
+
return project;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function deleteProject(yeaftDir, projectId) {
|
|
114
|
+
const projects = loadProjects(yeaftDir);
|
|
115
|
+
requireProject(projects, projectId);
|
|
116
|
+
saveProjects(yeaftDir, projects.filter(row => row.id !== projectId));
|
|
117
|
+
return { projectId };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function moveSessionToProject(yeaftDir, sessionId, projectId = null) {
|
|
121
|
+
const id = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
122
|
+
if (!id) throw new ProjectStoreError('invalid_session_id', 'Session id is required');
|
|
123
|
+
const projects = loadProjects(yeaftDir);
|
|
124
|
+
const target = projectId ? requireProject(projects, projectId) : null;
|
|
125
|
+
for (const project of projects) {
|
|
126
|
+
project.sessionIds = project.sessionIds.filter(value => value !== id);
|
|
127
|
+
}
|
|
128
|
+
if (target) target.sessionIds.push(id);
|
|
129
|
+
saveProjects(yeaftDir, projects);
|
|
130
|
+
return { sessionId: id, projectId: target?.id || null };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function removeSessionFromProjects(yeaftDir, sessionId) {
|
|
134
|
+
const projects = loadProjects(yeaftDir);
|
|
135
|
+
let changed = false;
|
|
136
|
+
for (const project of projects) {
|
|
137
|
+
const next = project.sessionIds.filter(id => id !== sessionId);
|
|
138
|
+
if (next.length !== project.sessionIds.length) changed = true;
|
|
139
|
+
project.sessionIds = next;
|
|
140
|
+
}
|
|
141
|
+
if (changed) saveProjects(yeaftDir, projects);
|
|
142
|
+
return changed;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function findProjectForSession(yeaftDir, sessionId) {
|
|
146
|
+
return loadProjects(yeaftDir).find(project => project.sessionIds.includes(sessionId)) || null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function sharedSessionIdsForProject(yeaftDir, sessionId) {
|
|
150
|
+
const project = findProjectForSession(yeaftDir, sessionId);
|
|
151
|
+
return project ? project.sessionIds.filter(id => id !== sessionId) : [];
|
|
152
|
+
}
|
|
@@ -259,6 +259,7 @@ export function formatPickedForInjection(picked) {
|
|
|
259
259
|
* @property {string[]} [currentTags] Contextual tags for rerank
|
|
260
260
|
* @property {number} [topK] Max FTS rows fetched (default 50)
|
|
261
261
|
* @property {number} [budgetTokens] Token budget for picked segments
|
|
262
|
+
* @property {number} [pickLimit] Max picked segments (default 8)
|
|
262
263
|
* @property {boolean} [fallbackOnEmpty] Include bounded recent scoped segments when FTS has no hits
|
|
263
264
|
* @property {number} [fallbackPerScope] Max fallback segments per scope
|
|
264
265
|
*/
|
|
@@ -353,6 +354,7 @@ export function runMemoryPreflow(index, opts) {
|
|
|
353
354
|
currentTags: opts.currentTags || [],
|
|
354
355
|
topK: opts.topK,
|
|
355
356
|
budgetTokens: opts.budgetTokens,
|
|
357
|
+
pickLimit: opts.pickLimit,
|
|
356
358
|
});
|
|
357
359
|
|
|
358
360
|
let fallbackUsed = false;
|
|
@@ -362,6 +364,7 @@ export function runMemoryPreflow(index, opts) {
|
|
|
362
364
|
ownVpId: opts.vpId || null,
|
|
363
365
|
budgetTokens: opts.budgetTokens,
|
|
364
366
|
perScope: opts.fallbackPerScope,
|
|
367
|
+
pickLimit: opts.pickLimit,
|
|
365
368
|
});
|
|
366
369
|
if (fallback.length > 0) {
|
|
367
370
|
fallbackUsed = true;
|
|
@@ -400,6 +403,7 @@ function fallbackScopedSegments(index, opts) {
|
|
|
400
403
|
const scopes = prioritizeFallbackScopes(filterScopes(opts.relevantScopes || [], opts.ownVpId || null));
|
|
401
404
|
const perScope = Number.isFinite(opts.perScope) && opts.perScope > 0 ? Math.floor(opts.perScope) : 2;
|
|
402
405
|
const budgetTokens = Number.isFinite(opts.budgetTokens) && opts.budgetTokens > 0 ? opts.budgetTokens : 1200;
|
|
406
|
+
const pickLimit = Number.isFinite(opts.pickLimit) && opts.pickLimit > 0 ? Math.floor(opts.pickLimit) : 8;
|
|
403
407
|
const buckets = [];
|
|
404
408
|
for (const scope of scopes) {
|
|
405
409
|
let segs = [];
|
|
@@ -419,6 +423,7 @@ function fallbackScopedSegments(index, opts) {
|
|
|
419
423
|
if (!seg) continue;
|
|
420
424
|
const tk = approxTokens(seg.body || '');
|
|
421
425
|
if (tk <= 0 || cost + tk > budgetTokens) continue;
|
|
426
|
+
if (out.length >= pickLimit) return out;
|
|
422
427
|
out.push(seg);
|
|
423
428
|
cost += tk;
|
|
424
429
|
}
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -64,6 +64,17 @@ import { persistYeaftAttachments, attachmentsForPersistence, persistedAttachment
|
|
|
64
64
|
import { normalizeSessionMessageQuote, sessionMessageQuotePrompt } from './session-message-quote.js';
|
|
65
65
|
import { ConversationStore, parseSeqFromId, projectVisibleSessionMessages } from './conversation/persist.js';
|
|
66
66
|
import { isHiddenConversationRow, isVisibleConversationRow } from './conversation/internal-control.js';
|
|
67
|
+
import {
|
|
68
|
+
ProjectStoreError,
|
|
69
|
+
createProject,
|
|
70
|
+
deleteProject,
|
|
71
|
+
loadProjects,
|
|
72
|
+
moveSessionToProject,
|
|
73
|
+
removeSessionFromProjects,
|
|
74
|
+
renameProject,
|
|
75
|
+
} from './projects/store.js';
|
|
76
|
+
import { readSummary as readScopeSummary } from './memory/store.js';
|
|
77
|
+
import { estimateTokens } from './dream/segment.js';
|
|
67
78
|
import { imageMetadataForPersistence } from './image-assets.js';
|
|
68
79
|
import { sliceLastNTurns } from './turn-utils.js';
|
|
69
80
|
import { pairSanitize } from './pair-sanitize.js';
|
|
@@ -3008,6 +3019,54 @@ function decorateSessionsWithRuntimeState(sessions) {
|
|
|
3008
3019
|
});
|
|
3009
3020
|
}
|
|
3010
3021
|
|
|
3022
|
+
const PROJECT_CONTEXT_MAX_SIBLINGS = 8;
|
|
3023
|
+
const PROJECT_CONTEXT_MAX_TOKENS = 4096;
|
|
3024
|
+
const PROJECT_CONTEXT_TRUNCATION_NOTICE = '\n[Summary truncated to Project context budget]';
|
|
3025
|
+
|
|
3026
|
+
async function sharedProjectContext(yeaftDir, sessionId, options = {}) {
|
|
3027
|
+
const project = loadProjects(yeaftDir).find(row => row.sessionIds.includes(sessionId));
|
|
3028
|
+
if (!project) return '';
|
|
3029
|
+
const memoryRoot = join(yeaftDir, 'memory');
|
|
3030
|
+
const language = options.language || 'en';
|
|
3031
|
+
const configuredBudget = Number.isFinite(options.tokenBudget) && options.tokenBudget > 0
|
|
3032
|
+
? options.tokenBudget
|
|
3033
|
+
: PROJECT_CONTEXT_MAX_TOKENS;
|
|
3034
|
+
const tokenBudget = Math.min(configuredBudget, PROJECT_CONTEXT_MAX_TOKENS);
|
|
3035
|
+
let context = '';
|
|
3036
|
+
const siblingIds = project.sessionIds
|
|
3037
|
+
.filter(id => id !== sessionId)
|
|
3038
|
+
.slice(0, PROJECT_CONTEXT_MAX_SIBLINGS);
|
|
3039
|
+
for (const siblingId of siblingIds) {
|
|
3040
|
+
const summary = await readScopeSummary(
|
|
3041
|
+
{ kind: 'session', id: siblingId },
|
|
3042
|
+
{ root: memoryRoot, language },
|
|
3043
|
+
).catch(() => '');
|
|
3044
|
+
if (!summary) continue;
|
|
3045
|
+
const separator = context ? '\n\n' : '';
|
|
3046
|
+
const header = `[Session ${siblingId}]\n`;
|
|
3047
|
+
const fullContext = `${context}${separator}${header}${summary}`;
|
|
3048
|
+
if (estimateTokens(fullContext) <= tokenBudget) {
|
|
3049
|
+
context = fullContext;
|
|
3050
|
+
continue;
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
const prefix = `${context}${separator}${header}`;
|
|
3054
|
+
if (estimateTokens(`${prefix}${PROJECT_CONTEXT_TRUNCATION_NOTICE}`) <= tokenBudget) {
|
|
3055
|
+
let low = 0;
|
|
3056
|
+
let high = summary.length;
|
|
3057
|
+
while (low < high) {
|
|
3058
|
+
const middle = Math.ceil((low + high) / 2);
|
|
3059
|
+
const candidate = `${prefix}${summary.slice(0, middle)}${PROJECT_CONTEXT_TRUNCATION_NOTICE}`;
|
|
3060
|
+
if (estimateTokens(candidate) <= tokenBudget) low = middle;
|
|
3061
|
+
else high = middle - 1;
|
|
3062
|
+
}
|
|
3063
|
+
context = `${prefix}${summary.slice(0, low)}${PROJECT_CONTEXT_TRUNCATION_NOTICE}`;
|
|
3064
|
+
}
|
|
3065
|
+
break;
|
|
3066
|
+
}
|
|
3067
|
+
return context;
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3011
3070
|
function sendSessionCrudResult(payload) {
|
|
3012
3071
|
const next = payload && payload.ok && Array.isArray(payload.sessions)
|
|
3013
3072
|
? { ...payload, sessions: decorateSessionsWithRuntimeState(payload.sessions) }
|
|
@@ -3020,7 +3079,7 @@ function sendSessionSnapshotBroadcast() {
|
|
|
3020
3079
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
3021
3080
|
if (!yeaftDir) return;
|
|
3022
3081
|
const sessions = decorateSessionsWithRuntimeState(snapshotSessions(yeaftDir));
|
|
3023
|
-
sendSessionEvent({ type: 'session_list_updated', sessions });
|
|
3082
|
+
sendSessionEvent({ type: 'session_list_updated', sessions, projects: loadProjects(yeaftDir) });
|
|
3024
3083
|
} catch (err) {
|
|
3025
3084
|
console.warn('[Yeaft] sendSessionSnapshotBroadcast failed:', err?.message || err);
|
|
3026
3085
|
}
|
|
@@ -3059,6 +3118,7 @@ function sessionErrorPayload(err) {
|
|
|
3059
3118
|
let code = 'unknown';
|
|
3060
3119
|
if (err instanceof SessionCrudError) code = err.code;
|
|
3061
3120
|
else if (err instanceof SessionConfigError) code = err.code;
|
|
3121
|
+
else if (err instanceof ProjectStoreError) code = err.code;
|
|
3062
3122
|
return {
|
|
3063
3123
|
code,
|
|
3064
3124
|
sessionId: err && err.sessionId,
|
|
@@ -3071,12 +3131,49 @@ export function handleYeaftListSessions(msg) {
|
|
|
3071
3131
|
try {
|
|
3072
3132
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
3073
3133
|
const groups = snapshotSessions(yeaftDir);
|
|
3074
|
-
sendSessionCrudResult({ op: 'list', requestId, ok: true, sessions: groups });
|
|
3134
|
+
sendSessionCrudResult({ op: 'list', requestId, ok: true, sessions: groups, projects: loadProjects(yeaftDir) });
|
|
3075
3135
|
} catch (err) {
|
|
3076
3136
|
sendSessionCrudResult({ op: 'list', requestId, ok: false, error: sessionErrorPayload(err) });
|
|
3077
3137
|
}
|
|
3078
3138
|
}
|
|
3079
3139
|
|
|
3140
|
+
export function handleYeaftProjectMutation(msg) {
|
|
3141
|
+
const requestId = msg && msg.requestId;
|
|
3142
|
+
const op = msg && msg.op;
|
|
3143
|
+
try {
|
|
3144
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
3145
|
+
let result = null;
|
|
3146
|
+
if (op === 'create') result = createProject(yeaftDir, msg.name);
|
|
3147
|
+
else if (op === 'rename') result = renameProject(yeaftDir, msg.projectId, msg.name);
|
|
3148
|
+
else if (op === 'delete') result = deleteProject(yeaftDir, msg.projectId);
|
|
3149
|
+
else if (op === 'move_session') {
|
|
3150
|
+
if (!snapshotSessions(yeaftDir).some(row => row.id === msg.sessionId)) {
|
|
3151
|
+
throw new ProjectStoreError('session_not_found', 'Session not found');
|
|
3152
|
+
}
|
|
3153
|
+
result = moveSessionToProject(yeaftDir, msg.sessionId, msg.projectId || null);
|
|
3154
|
+
} else {
|
|
3155
|
+
throw new ProjectStoreError('invalid_op', 'Unknown Project operation');
|
|
3156
|
+
}
|
|
3157
|
+
sendSessionEvent({
|
|
3158
|
+
type: 'project_mutation_result',
|
|
3159
|
+
requestId,
|
|
3160
|
+
op,
|
|
3161
|
+
ok: true,
|
|
3162
|
+
result,
|
|
3163
|
+
projects: loadProjects(yeaftDir),
|
|
3164
|
+
}, { requestId });
|
|
3165
|
+
sendSessionSnapshotBroadcast();
|
|
3166
|
+
} catch (err) {
|
|
3167
|
+
sendSessionEvent({
|
|
3168
|
+
type: 'project_mutation_result',
|
|
3169
|
+
requestId,
|
|
3170
|
+
op,
|
|
3171
|
+
ok: false,
|
|
3172
|
+
error: sessionErrorPayload(err),
|
|
3173
|
+
}, { requestId });
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3080
3177
|
export function handleYeaftCreateSession(msg) {
|
|
3081
3178
|
const requestId = msg && msg.requestId;
|
|
3082
3179
|
const payload = (msg && msg.payload) || {};
|
|
@@ -3260,6 +3357,7 @@ export function handleYeaftDeleteSession(msg) {
|
|
|
3260
3357
|
try {
|
|
3261
3358
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
3262
3359
|
const result = deleteSession(yeaftDir, sessionId);
|
|
3360
|
+
removeSessionFromProjects(yeaftDir, sessionId);
|
|
3263
3361
|
ctx.assetOutbox?.removeSession(sessionId);
|
|
3264
3362
|
// Cascade: remove every persisted message stamped with this group id.
|
|
3265
3363
|
// Hard delete (per user spec): no soft-archive, the bytes are gone.
|
|
@@ -3978,6 +4076,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
3978
4076
|
type: 'memory_used',
|
|
3979
4077
|
turnId: event.turnId,
|
|
3980
4078
|
loaded: event.loaded || [],
|
|
4079
|
+
meta: event.meta || null,
|
|
3981
4080
|
}, envelope);
|
|
3982
4081
|
break;
|
|
3983
4082
|
|
|
@@ -5054,6 +5153,18 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
5054
5153
|
envelope: inboundEnvelope,
|
|
5055
5154
|
threadId,
|
|
5056
5155
|
});
|
|
5156
|
+
if (queryOpts) {
|
|
5157
|
+
const projectContext = await sharedProjectContext(ctx.CONFIG?.yeaftDir, sessionId, {
|
|
5158
|
+
language: session?.config?.language,
|
|
5159
|
+
tokenBudget: Math.max(512, Math.floor((session?.config?.messageTokenBudget || 32768) / 8)),
|
|
5160
|
+
});
|
|
5161
|
+
if (projectContext) {
|
|
5162
|
+
const sharedBlock = `[Project Shared Context]\nRead-only memory summaries from sibling Sessions in the same Project. Preserve each source Session identity.\n\n${projectContext}`;
|
|
5163
|
+
queryOpts.sessionAnnouncement = queryOpts.sessionAnnouncement
|
|
5164
|
+
? `${queryOpts.sessionAnnouncement}\n\n${sharedBlock}`
|
|
5165
|
+
: sharedBlock;
|
|
5166
|
+
}
|
|
5167
|
+
}
|
|
5057
5168
|
let turnSessionMeta = null;
|
|
5058
5169
|
try { turnSessionMeta = sessionCoordinator?.group?.getMeta?.() || null; } catch { turnSessionMeta = null; }
|
|
5059
5170
|
const projectRuntime = getProjectRuntimeForTurn(turnSessionMeta);
|
|
@@ -7171,6 +7282,8 @@ export async function handleYeaftMcpReload(msg = {}) {
|
|
|
7171
7282
|
}
|
|
7172
7283
|
|
|
7173
7284
|
export const __testHooks = {
|
|
7285
|
+
loadProjects,
|
|
7286
|
+
sharedProjectContext,
|
|
7174
7287
|
loadVisibleGroupHistoryPage,
|
|
7175
7288
|
projectVisibleHistoryChunkMessages,
|
|
7176
7289
|
persistInboundMessageOnceByMsgId,
|