@yeaft/webchat-agent 1.0.120 → 1.0.122
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 +32 -27
- package/package.json +1 -1
- package/yeaft/engine.js +4 -4
- package/yeaft/sessions/pre-flow.js +20 -6
- package/yeaft/status-cache.js +28 -4
|
@@ -30,7 +30,34 @@ import { getLlmConfig, updateLlmConfig, getYeaftSettings, updateYeaftSettings, g
|
|
|
30
30
|
import { discoverLlmModels } from '../llm-model-discovery.js';
|
|
31
31
|
import { fetchModelsDev } from '../yeaft/llm/models-dev.js';
|
|
32
32
|
import { handleYeaftSessionSend, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager, preloadYeaftSkillSlashCommands } from '../yeaft/web-bridge.js';
|
|
33
|
-
import { startYeaftStatusRefresh,
|
|
33
|
+
import { startYeaftStatusRefresh, forceRefreshYeaftStatus } from '../yeaft/status-cache.js';
|
|
34
|
+
|
|
35
|
+
export async function applyLlmConfigUpdate(msg, dependencies = {}) {
|
|
36
|
+
const updateConfig = dependencies.updateLlmConfig || updateLlmConfig;
|
|
37
|
+
const broadcastLanguage = dependencies.broadcastLanguageChange || broadcastLanguageChange;
|
|
38
|
+
const forceStatusRefresh = dependencies.forceRefreshYeaftStatus || forceRefreshYeaftStatus;
|
|
39
|
+
const send = dependencies.sendToServer || sendToServer;
|
|
40
|
+
const yeaftDir = dependencies.yeaftDir ?? ctx.CONFIG?.yeaftDir;
|
|
41
|
+
const incomingLanguage = typeof msg.config?.language === 'string' && msg.config.language
|
|
42
|
+
? msg.config.language
|
|
43
|
+
: null;
|
|
44
|
+
const result = updateConfig(msg.config || {}, yeaftDir);
|
|
45
|
+
if (!result.error && incomingLanguage) broadcastLanguage(result.language);
|
|
46
|
+
|
|
47
|
+
let statusRefreshError = null;
|
|
48
|
+
if (!result.error) {
|
|
49
|
+
try {
|
|
50
|
+
const statusEvent = await forceStatusRefresh({ reason: 'llm_config_updated' });
|
|
51
|
+
statusRefreshError = statusEvent?.refreshError || null;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
statusRefreshError = err?.message || String(err);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const response = { type: 'llm_config_updated', ...result, statusRefreshError };
|
|
58
|
+
send(response);
|
|
59
|
+
return response;
|
|
60
|
+
}
|
|
34
61
|
|
|
35
62
|
export async function handleMessage(msg) {
|
|
36
63
|
switch (msg.type) {
|
|
@@ -326,32 +353,10 @@ export async function handleMessage(msg) {
|
|
|
326
353
|
}
|
|
327
354
|
|
|
328
355
|
case 'update_llm_config': {
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
// the *input* `language` field instead.
|
|
334
|
-
const incomingLanguage = typeof msg.config?.language === 'string' && msg.config.language
|
|
335
|
-
? msg.config.language
|
|
336
|
-
: null;
|
|
337
|
-
const result = updateLlmConfig(msg.config || {}, ctx.CONFIG?.yeaftDir);
|
|
338
|
-
// task-708: live locale propagation. When the user flips the UI
|
|
339
|
-
// language dropdown, push the new value into every cached Engine
|
|
340
|
-
// (per-VP pool + 1:1 chat session.engine) so the very next turn
|
|
341
|
-
// renders the system prompt in the chosen language without the
|
|
342
|
-
// user reloading the session.
|
|
343
|
-
if (!result.error && incomingLanguage) {
|
|
344
|
-
broadcastLanguageChange(result.language);
|
|
345
|
-
}
|
|
346
|
-
if (!result.error) {
|
|
347
|
-
refreshYeaftStatus({ reason: 'llm_config_updated' }).catch(() => {});
|
|
348
|
-
if (!incomingLanguage) {
|
|
349
|
-
resetYeaftSession().catch(err => {
|
|
350
|
-
console.error('[LLM] Failed to reload Yeaft session after local config update:', err.message);
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
sendToServer({ type: 'llm_config_updated', ...result });
|
|
356
|
+
// Saving the file and refreshing the model menu are separate outcomes.
|
|
357
|
+
// Always acknowledge the completed write; the frontend owns the single
|
|
358
|
+
// runtime reset it dispatches after a successful save.
|
|
359
|
+
await applyLlmConfigUpdate(msg);
|
|
355
360
|
break;
|
|
356
361
|
}
|
|
357
362
|
|
package/package.json
CHANGED
package/yeaft/engine.js
CHANGED
|
@@ -23,7 +23,7 @@ import { join, resolve as resolvePath } from 'path';
|
|
|
23
23
|
import { buildSystemPrompt, buildWorkerPrompt } from './prompts.js';
|
|
24
24
|
import { getRuntimePlatformInfo } from './runtime-platform.js';
|
|
25
25
|
import { LLMContextError, LLMAbortError, LLMRateLimitError, LLMServerError, LLMStreamIdleTimeoutError } from './llm/adapter.js';
|
|
26
|
-
import { runMemoryPreflow, buildRelevantScopes } from './sessions/pre-flow.js';
|
|
26
|
+
import { runMemoryPreflow, buildRelevantScopes, memoryScopeLabel } from './sessions/pre-flow.js';
|
|
27
27
|
import { readProjectDoc, pickProjectDocFile, DEFAULT_PROJECT_DOC_MAX_BYTES } from './sessions/project-doc.js';
|
|
28
28
|
import { partitionMessages } from './compact/partition.js';
|
|
29
29
|
import { runCompact as runCompactOrchestrator } from './compact/orchestrator.js';
|
|
@@ -905,19 +905,19 @@ export class Engine {
|
|
|
905
905
|
if (snap.resident.length > 0) {
|
|
906
906
|
parts.push(zh ? '### 常驻记忆' : '### Resident');
|
|
907
907
|
for (const r of snap.resident) {
|
|
908
|
-
parts.push(`- **${r.scope}**: ${r.summary}`);
|
|
908
|
+
parts.push(`- **${memoryScopeLabel(r.scope)}**: ${r.summary}`);
|
|
909
909
|
}
|
|
910
910
|
}
|
|
911
911
|
if (snap.recent.length > 0) {
|
|
912
912
|
parts.push(zh ? '### 最近记忆' : '### Recent');
|
|
913
913
|
for (const s of snap.recent) {
|
|
914
|
-
parts.push(`- (${s.scope}) ${(s.body || '').trim()}`);
|
|
914
|
+
parts.push(`- (${memoryScopeLabel(s.scope)}) ${(s.body || '').trim()}`);
|
|
915
915
|
}
|
|
916
916
|
}
|
|
917
917
|
if (snap.onDemand.length > 0) {
|
|
918
918
|
parts.push(zh ? '### 按需记忆' : '### OnDemand');
|
|
919
919
|
for (const s of snap.onDemand) {
|
|
920
|
-
parts.push(`- (${s.scope}) ${(s.body || '').trim()}`);
|
|
920
|
+
parts.push(`- (${memoryScopeLabel(s.scope)}) ${(s.body || '').trim()}`);
|
|
921
921
|
}
|
|
922
922
|
}
|
|
923
923
|
return parts.join('\n');
|
|
@@ -157,6 +157,24 @@ export function selectRespondingVps(input) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Turn an internal memory scope into the compact label shown to the model.
|
|
162
|
+
* The active-scope prompt already carries the current session id, so repeating
|
|
163
|
+
* the full storage path on every memory entry only wastes context. Internal
|
|
164
|
+
* entries and debug events keep the original scope for ACLs and diagnostics.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} scope
|
|
167
|
+
* @returns {string}
|
|
168
|
+
*/
|
|
169
|
+
export function memoryScopeLabel(scope) {
|
|
170
|
+
const value = typeof scope === 'string' && scope ? scope : 'unknown';
|
|
171
|
+
const sessionTopic = /^(?:sessions|session|group)\/[^/]+\/topic\/(.+)$/.exec(value);
|
|
172
|
+
if (sessionTopic) return `topic: ${sessionTopic[1]}`;
|
|
173
|
+
if (/^(?:sessions|session|group)\/[^/]+$/.test(value)) return 'session';
|
|
174
|
+
if (value.startsWith('topic/')) return `topic: ${value.slice(6)}`;
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
|
|
160
178
|
/**
|
|
161
179
|
* Build the heading for a single scope's formatted memory block.
|
|
162
180
|
*
|
|
@@ -167,6 +185,8 @@ export function selectRespondingVps(input) {
|
|
|
167
185
|
* @returns {string}
|
|
168
186
|
*/
|
|
169
187
|
function scopeHeading(scope) {
|
|
188
|
+
const label = memoryScopeLabel(scope);
|
|
189
|
+
if (label.startsWith('topic: ')) return `## Memory: Topic ${label.slice(7)}`;
|
|
170
190
|
if (scope === 'user') return '## Memory: User';
|
|
171
191
|
// Nested chat scopes first.
|
|
172
192
|
let m = /^chat\/([^/]+)\/vp\/(.+)$/.exec(scope);
|
|
@@ -180,10 +200,6 @@ function scopeHeading(scope) {
|
|
|
180
200
|
if (m) return `## Memory: Session ${m[1]} (user)`;
|
|
181
201
|
m = /^session\/([^/]+)\/feature\/(.+)$/.exec(scope);
|
|
182
202
|
if (m) return `## Memory: Feature ${m[2]}`;
|
|
183
|
-
m = /^sessions\/([^/]+)\/topic\/(.+)$/.exec(scope);
|
|
184
|
-
if (m) return `## Memory: Topic ${m[2]}`;
|
|
185
|
-
m = /^session\/([^/]+)\/topic\/(.+)$/.exec(scope);
|
|
186
|
-
if (m) return `## Memory: Topic ${m[2]}`;
|
|
187
203
|
// Legacy nested group scopes (un-migrated data).
|
|
188
204
|
m = /^group\/([^/]+)\/vp\/(.+)$/.exec(scope);
|
|
189
205
|
if (m) return `## Memory: VP ${m[2]}`;
|
|
@@ -191,8 +207,6 @@ function scopeHeading(scope) {
|
|
|
191
207
|
if (m) return `## Memory: Session ${m[1]} (user)`;
|
|
192
208
|
m = /^group\/([^/]+)\/feature\/(.+)$/.exec(scope);
|
|
193
209
|
if (m) return `## Memory: Feature ${m[2]}`;
|
|
194
|
-
m = /^group\/([^/]+)\/topic\/(.+)$/.exec(scope);
|
|
195
|
-
if (m) return `## Memory: Topic ${m[2]}`;
|
|
196
210
|
if (scope.startsWith('session/')) return `## Memory: Session ${scope.slice(8)}`;
|
|
197
211
|
if (scope.startsWith('group/')) return `## Memory: Session ${scope.slice(6)}`;
|
|
198
212
|
if (scope.startsWith('vp/')) return `## Memory: VP ${scope.slice(3)}`;
|
package/yeaft/status-cache.js
CHANGED
|
@@ -53,6 +53,7 @@ export function createYeaftStatusCache(options = {}) {
|
|
|
53
53
|
let snapshot = null;
|
|
54
54
|
let timer = null;
|
|
55
55
|
let inFlight = null;
|
|
56
|
+
let generation = 0;
|
|
56
57
|
|
|
57
58
|
function current() {
|
|
58
59
|
return snapshot ? { ...snapshot, availableModels: normalizeAvailableModels(snapshot.availableModels) } : null;
|
|
@@ -68,14 +69,16 @@ export function createYeaftStatusCache(options = {}) {
|
|
|
68
69
|
async function refresh({ reason = 'manual', emitRefreshing = true, sessionStatus = null } = {}) {
|
|
69
70
|
if (inFlight) return inFlight;
|
|
70
71
|
const startedAt = now();
|
|
72
|
+
const refreshGeneration = generation;
|
|
71
73
|
if (emitRefreshing && snapshot) {
|
|
72
74
|
snapshot = { ...snapshot, refreshing: true, refreshStartedAt: startedAt, refreshReason: reason };
|
|
73
75
|
emitSnapshot();
|
|
74
76
|
}
|
|
75
|
-
|
|
77
|
+
const refreshPromise = Promise.resolve()
|
|
76
78
|
.then(async () => {
|
|
77
79
|
const yeaftDir = options.getYeaftDir ? options.getYeaftDir() : ctx.CONFIG?.yeaftDir;
|
|
78
80
|
const config = await load({ ...(yeaftDir && { dir: yeaftDir }) });
|
|
81
|
+
if (refreshGeneration !== generation) return current();
|
|
79
82
|
const previous = snapshot || {};
|
|
80
83
|
snapshot = {
|
|
81
84
|
...previous,
|
|
@@ -94,6 +97,7 @@ export function createYeaftStatusCache(options = {}) {
|
|
|
94
97
|
return emitSnapshot();
|
|
95
98
|
})
|
|
96
99
|
.catch((err) => {
|
|
100
|
+
if (refreshGeneration !== generation) return current();
|
|
97
101
|
const message = err?.message || String(err);
|
|
98
102
|
const previous = snapshot || {};
|
|
99
103
|
snapshot = {
|
|
@@ -107,12 +111,28 @@ export function createYeaftStatusCache(options = {}) {
|
|
|
107
111
|
};
|
|
108
112
|
return emitSnapshot();
|
|
109
113
|
})
|
|
110
|
-
.finally(() => {
|
|
111
|
-
|
|
114
|
+
.finally(() => {
|
|
115
|
+
if (inFlight === refreshPromise) inFlight = null;
|
|
116
|
+
});
|
|
117
|
+
inFlight = refreshPromise;
|
|
118
|
+
return refreshPromise;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function forceRefresh(options = {}) {
|
|
122
|
+
// Invalidate any disk read that started before the config write, wait for it
|
|
123
|
+
// to drain, then start a new read. A plain refresh() intentionally dedupes
|
|
124
|
+
// concurrent callers, which is wrong after a successful config update.
|
|
125
|
+
generation += 1;
|
|
126
|
+
if (inFlight) await inFlight;
|
|
127
|
+
return refresh({ ...options, emitRefreshing: options.emitRefreshing ?? false });
|
|
112
128
|
}
|
|
113
129
|
|
|
114
130
|
function hydrateFromSession(sessionLike, { reason = 'session_ready', emitEvent = true } = {}) {
|
|
115
131
|
if (!sessionLike) return null;
|
|
132
|
+
// Session hydration is authoritative. Any refresh that started before this
|
|
133
|
+
// point loaded an older disk snapshot and must not overwrite it when it
|
|
134
|
+
// resolves later.
|
|
135
|
+
generation += 1;
|
|
116
136
|
const previous = snapshot || {};
|
|
117
137
|
snapshot = {
|
|
118
138
|
...previous,
|
|
@@ -144,7 +164,7 @@ export function createYeaftStatusCache(options = {}) {
|
|
|
144
164
|
timer = null;
|
|
145
165
|
}
|
|
146
166
|
|
|
147
|
-
return { current, refresh, hydrateFromSession, start, stop, emitSnapshot };
|
|
167
|
+
return { current, refresh, forceRefresh, hydrateFromSession, start, stop, emitSnapshot };
|
|
148
168
|
}
|
|
149
169
|
|
|
150
170
|
export const yeaftStatusCache = createYeaftStatusCache();
|
|
@@ -161,6 +181,10 @@ export function refreshYeaftStatus(options) {
|
|
|
161
181
|
return yeaftStatusCache.refresh(options);
|
|
162
182
|
}
|
|
163
183
|
|
|
184
|
+
export function forceRefreshYeaftStatus(options) {
|
|
185
|
+
return yeaftStatusCache.forceRefresh(options);
|
|
186
|
+
}
|
|
187
|
+
|
|
164
188
|
export function hydrateYeaftStatusFromSession(sessionLike, options) {
|
|
165
189
|
return yeaftStatusCache.hydrateFromSession(sessionLike, options);
|
|
166
190
|
}
|