@yeaft/webchat-agent 1.0.292 → 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 +178 -149
- 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/projects/store.js +152 -0
- package/yeaft/web-bridge.js +114 -2
|
Binary file
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|
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.
|
|
@@ -5055,6 +5153,18 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
5055
5153
|
envelope: inboundEnvelope,
|
|
5056
5154
|
threadId,
|
|
5057
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
|
+
}
|
|
5058
5168
|
let turnSessionMeta = null;
|
|
5059
5169
|
try { turnSessionMeta = sessionCoordinator?.group?.getMeta?.() || null; } catch { turnSessionMeta = null; }
|
|
5060
5170
|
const projectRuntime = getProjectRuntimeForTurn(turnSessionMeta);
|
|
@@ -7172,6 +7282,8 @@ export async function handleYeaftMcpReload(msg = {}) {
|
|
|
7172
7282
|
}
|
|
7173
7283
|
|
|
7174
7284
|
export const __testHooks = {
|
|
7285
|
+
loadProjects,
|
|
7286
|
+
sharedProjectContext,
|
|
7175
7287
|
loadVisibleGroupHistoryPage,
|
|
7176
7288
|
projectVisibleHistoryChunkMessages,
|
|
7177
7289
|
persistInboundMessageOnceByMsgId,
|