@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.292",
3
+ "version": "1.0.293",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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
+ }
@@ -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,