@yeaft/webchat-agent 1.0.121 → 1.0.123

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.
@@ -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, refreshYeaftStatus } from '../yeaft/status-cache.js';
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
- // Capture the user's intent BEFORE updateLlmConfig the return
330
- // envelope ALWAYS populates `language` (falls back to 'en'), so
331
- // gating on the *output* would broadcast on every provider /
332
- // primaryModel / fastModel save, not just locale flips. Gate on
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.121",
3
+ "version": "1.0.123",
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",
@@ -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
- inFlight = Promise.resolve()
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(() => { inFlight = null; });
111
- return inFlight;
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
  }