@yeaft/webchat-agent 1.0.91 → 1.0.95

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.91",
3
+ "version": "1.0.95",
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",
@@ -10,7 +10,8 @@
10
10
  import { join } from 'node:path';
11
11
 
12
12
  import { readMemory, readSummary } from '../memory/store.js';
13
- import { readSessionState } from './state.js';
13
+ import { readDreamError, readSessionState } from './state.js';
14
+ import { buildRunDreamOpts } from './session-wiring.js';
14
15
 
15
16
  export const DREAM_SNAPSHOT_TEXT_LIMIT = 6000;
16
17
 
@@ -32,7 +33,7 @@ export async function buildDreamOutputSnapshot(sessionLike, sessionId) {
32
33
  const scope = `sessions/${sessionId}`;
33
34
  const memoryScope = { kind: 'session', id: sessionId };
34
35
  const root = join(sessionLike.yeaftDir, 'memory');
35
- const [memoryRaw, summaryRaw, state] = await Promise.all([
36
+ const [memoryRaw, summaryRaw, state, lastError] = await Promise.all([
36
37
  readMemory(memoryScope, { root }).catch(() => ''),
37
38
  readSummary(memoryScope, { root }).catch(() => ''),
38
39
  readSessionState(root, sessionId).catch(() => ({
@@ -40,7 +41,9 @@ export async function buildDreamOutputSnapshot(sessionLike, sessionId) {
40
41
  lastDreamAt: null,
41
42
  messageCount: 0,
42
43
  })),
44
+ readDreamError(root, scope).catch(() => null),
43
45
  ]);
46
+ const totalMessageCount = await countSessionMessages(sessionLike, sessionId);
44
47
  const memory = truncateDreamText(memoryRaw);
45
48
  const summary = truncateDreamText(summaryRaw);
46
49
  return {
@@ -50,10 +53,26 @@ export async function buildDreamOutputSnapshot(sessionLike, sessionId) {
50
53
  lastDreamAt: state?.lastDreamAt || null,
51
54
  lastDreamMessageId: state?.lastDreamMessageId || null,
52
55
  messageCount: Number.isFinite(state?.messageCount) ? state.messageCount : 0,
56
+ totalMessageCount,
53
57
  hasOutput: !!(memoryRaw || summaryRaw),
58
+ lastError,
54
59
  memoryText: memory.text,
55
60
  memoryTruncated: memory.truncated,
56
61
  summaryText: summary.text,
57
62
  summaryTruncated: summary.truncated,
58
63
  };
59
64
  }
65
+
66
+ async function countSessionMessages(sessionLike, sessionId) {
67
+ try {
68
+ const opts = buildRunDreamOpts({
69
+ yeaftDir: sessionLike.yeaftDir,
70
+ config: sessionLike.config || {},
71
+ adapter: { call: async () => ({ text: '', usage: {} }) },
72
+ });
73
+ const n = await opts.countMessages(sessionId);
74
+ return Number.isFinite(n) ? n : 0;
75
+ } catch {
76
+ return 0;
77
+ }
78
+ }
@@ -45,7 +45,7 @@ import { listScopes, readSummary } from '../memory/store.js';
45
45
  import {
46
46
  DEFAULT_LIMITS,
47
47
  } from './limits.js';
48
- import { readSessionState, writeSessionState, writeDreamError } from './state.js';
48
+ import { clearDreamError, readSessionState, writeSessionState, writeDreamError } from './state.js';
49
49
  import { segmentDiff, truncateMessage, estimateMessagesTokens } from './segment.js';
50
50
  import { triageGroupSegments } from './triage.js';
51
51
  import { mergeByTarget } from './merge.js';
@@ -166,7 +166,7 @@ export async function runDream(opts) {
166
166
  sessionId,
167
167
  segments,
168
168
  topicSummaries,
169
- llm: opts.llm,
169
+ llm: dreamLlmForSession(opts.llm, sessionId),
170
170
  onProgress,
171
171
  language: opts.language,
172
172
  });
@@ -208,13 +208,14 @@ export async function runDream(opts) {
208
208
  const r = await applyMergedTarget(merged, {
209
209
  root: opts.root,
210
210
  ts,
211
- llm: opts.llm,
211
+ llm: dreamLlmForSession(opts.llm, sourceSessionId(merged)),
212
212
  limits,
213
213
  nowIso: opts.nowIso || (() => nowIso),
214
214
  onProgress,
215
215
  siblingTopicsFor: opts.siblingTopicsFor,
216
216
  language: opts.language,
217
217
  });
218
+ await clearDreamError(opts.root, merged.target);
218
219
  targetsReport.push({ ...r, sources: merged.sources.length, status: 'done' });
219
220
  } catch (err) {
220
221
  targetsReport.push({
@@ -252,11 +253,12 @@ export async function runDream(opts) {
252
253
  sessionId: triage.sessionId,
253
254
  messages: triage.diff,
254
255
  targets,
255
- llm: opts.llm,
256
+ llm: dreamLlmForSession(opts.llm, triage.sessionId),
256
257
  language: opts.language,
257
258
  nowIso: opts.nowIso || (() => nowIso),
258
259
  segmentIndex: opts.segmentIndex || null,
259
260
  });
261
+ await clearDreamError(opts.root, `sessions/${triage.sessionId}`);
260
262
  segmentReports.push({ sessionId: triage.sessionId, status: 'done', ...r });
261
263
  onProgress({ phase: 'extract-segments', sessionId: triage.sessionId, status: 'done', ...r });
262
264
  } catch (err) {
@@ -335,6 +337,10 @@ function deriveSessionFilter(filter) {
335
337
  return sessions.length > 0 ? new Set(sessions) : null;
336
338
  }
337
339
 
340
+ function dreamLlmForSession(llm, sessionId) {
341
+ return (req = {}) => llm({ ...req, sessionId: req.sessionId || sessionId || null });
342
+ }
343
+
338
344
  function sourceSessionId(mergedTarget) {
339
345
  const src = Array.isArray(mergedTarget?.sources) ? mergedTarget.sources[0] : null;
340
346
  return src && typeof src.sessionId === 'string' ? src.sessionId : '';
@@ -168,9 +168,7 @@ function normalizeExtractedSegment({ item, scope, now }) {
168
168
  if (!body) return null;
169
169
  const kind = VALID_KINDS.has(String(item.kind || '')) ? String(item.kind) : 'context';
170
170
  const tags = Array.isArray(item.tags) ? item.tags.map(t => String(t).trim()).filter(Boolean) : [];
171
- const sourceMessages = Array.isArray(item.sourceMessages)
172
- ? item.sourceMessages.map(id => String(id).trim()).filter(Boolean)
173
- : [];
171
+ const sourceMessages = normalizeSourceMessageIds(item.sourceMessages);
174
172
  return makeSegment({
175
173
  scope,
176
174
  kind,
@@ -182,6 +180,23 @@ function normalizeExtractedSegment({ item, scope, now }) {
182
180
  });
183
181
  }
184
182
 
183
+ function normalizeSourceMessageIds(value) {
184
+ if (!Array.isArray(value)) return [];
185
+ const ids = [];
186
+ for (const item of value) {
187
+ if (typeof item === 'string' || typeof item === 'number') {
188
+ const id = String(item).trim();
189
+ if (id) ids.push(id);
190
+ continue;
191
+ }
192
+ if (item && typeof item === 'object') {
193
+ const id = String(item.id || item.messageId || item.uuid || '').trim();
194
+ if (id) ids.push(id);
195
+ }
196
+ }
197
+ return ids;
198
+ }
199
+
185
200
  function buildRecentSegment({ scope, messages, now }) {
186
201
  const recentMessages = messages.slice(-RECENT_MESSAGE_COUNT);
187
202
  const body = [
@@ -43,6 +43,8 @@ import { join } from 'path';
43
43
  import { runDream } from './runner.js';
44
44
  import { createDreamScheduler } from './schedule.js';
45
45
  import { parseMessage, parseSeqFromId } from '../conversation/persist.js';
46
+ import { loadSessionConfig, resolveSessionConfig } from '../sessions/session-config.js';
47
+ import { listSessions as listSessionMetas } from '../sessions/session-store.js';
46
48
  import { readSessionState } from './state.js';
47
49
  import { DREAM_NUDGE_AFTER_MESSAGES, DREAM_INTERVAL_HOURS } from './limits.js';
48
50
 
@@ -83,7 +85,7 @@ export function buildRunDreamOpts(session, onProgress) {
83
85
  segmentIndex: session.memoryIndex || null,
84
86
  llm: makeLlm(session),
85
87
  listSessions: async () => {
86
- try { return listConversationSessions([sessionConversationsRoot, legacySessionConversationsRoot]); }
88
+ try { return listRegisteredSessions([sessionConversationsRoot, legacySessionConversationsRoot]); }
87
89
  catch { return []; }
88
90
  },
89
91
  countMessages: async (sessionId) => {
@@ -132,15 +134,25 @@ function translateSessionConversationMessage(m) {
132
134
  }
133
135
 
134
136
  /**
135
- * Enumerate session ids that have persisted conversation messages. `sessions/`
136
- * is the primary layout; `groups/` is read-only legacy fallback.
137
+ * Enumerate sessions from authoritative session metadata first, then fall back
138
+ * to legacy transcript-only directories. Message presence is not used as the
139
+ * source of truth: a stale/corrupt JSONL index must not make Dream forget that
140
+ * a Session exists. The runner's .dream-state cursor decides whether there is
141
+ * anything new to process.
137
142
  *
138
143
  * @param {string[]} roots session transcript roots in priority order
139
144
  * @returns {string[]}
140
145
  */
141
- function listConversationSessions(roots) {
146
+ function listRegisteredSessions(roots) {
142
147
  const out = new Set();
143
- for (const root of roots) {
148
+ const [sessionsRoot, ...legacyRoots] = roots;
149
+ try {
150
+ for (const meta of listSessionMetas(sessionsRoot)) {
151
+ if (meta?.id) out.add(meta.id);
152
+ }
153
+ } catch { /* fall through to legacy fallback */ }
154
+
155
+ for (const root of legacyRoots) {
144
156
  if (!existsSync(root)) continue;
145
157
  for (const name of readdirSync(root)) {
146
158
  if (name.startsWith('.')) continue;
@@ -149,8 +161,8 @@ function listConversationSessions(roots) {
149
161
  if (!hasReadableMessages(dir)) continue;
150
162
  out.add(name);
151
163
  } catch {
152
- // Ignore partial or old session directories. Dream only needs sessions
153
- // with readable conversation messages.
164
+ // Ignore partial old directories. Canonical sessions come from
165
+ // sessions/<id>/session.json above; this branch is read-only legacy.
154
166
  }
155
167
  }
156
168
  }
@@ -210,14 +222,13 @@ function hasJsonlMessages(dir) {
210
222
 
211
223
  function loadSessionDiskMessages(dir, sessionId) {
212
224
  const conversationDir = join(dir, 'conversation');
213
- return [
214
- // Prefer the canonical ConversationStore projection when it exists: it has
215
- // normalized role/content/tool metadata. The session append-only audit log
216
- // is still load-bearing for sessions created before/while conversation rows
217
- // were being migrated to JSONL.
218
- ...loadConversationDirMessages(conversationDir),
219
- ...loadSessionJsonlMessages(join(dir, 'messages'), sessionId),
220
- ];
225
+ // Canonical conversation history is the source Dream needs: it contains the
226
+ // full user/assistant timeline. The session audit log under messages/ is a
227
+ // fallback for older or partially migrated sessions, not a second source to
228
+ // union in otherwise the same user turn can appear twice with different ids.
229
+ const canonical = loadConversationDirMessages(conversationDir);
230
+ if (canonical.length > 0) return canonical;
231
+ return loadSessionJsonlMessages(join(dir, 'messages'), sessionId);
221
232
  }
222
233
 
223
234
  function loadConversationDirMessages(conversationDir) {
@@ -302,6 +313,24 @@ function normalizeSessionJsonlMessage(row, sessionId) {
302
313
  function stringifyMessageBody(value) {
303
314
  if (typeof value === 'string') return value;
304
315
  if (value == null) return '';
316
+ if (Array.isArray(value)) return value.map(stringifyMessageBody).filter(Boolean).join('\n');
317
+ if (typeof value === 'object') {
318
+ if (typeof value.text === 'string') return value.text;
319
+ if (typeof value.content === 'string') return value.content;
320
+ if (typeof value.body === 'string') return value.body;
321
+ if (typeof value.value === 'string') return value.value;
322
+ if (Array.isArray(value.content)) return stringifyMessageBody(value.content);
323
+ if (Array.isArray(value.parts)) return stringifyMessageBody(value.parts);
324
+ if (Array.isArray(value.blocks)) return stringifyMessageBody(value.blocks);
325
+ if (value.type && (value.type === 'image' || value.type === 'image_url')) return '[image]';
326
+ if (value.type && (value.type === 'tool_use' || value.type === 'tool_call')) {
327
+ const name = value.name || value.toolName || value.function?.name || 'tool';
328
+ return `[tool call: ${name}]`;
329
+ }
330
+ if (value.type && (value.type === 'tool_result' || value.type === 'function_result')) {
331
+ return stringifyMessageBody(value.result ?? value.output ?? value.content ?? '[tool result]');
332
+ }
333
+ }
305
334
  try { return JSON.stringify(value); } catch { return String(value); }
306
335
  }
307
336
 
@@ -323,9 +352,13 @@ function safeDirComponent(s) {
323
352
  * @param {Object} session
324
353
  */
325
354
  function makeLlm(session) {
326
- return async ({ pass, prompt, system }) => {
355
+ return async ({ pass, prompt, system, sessionId }) => {
327
356
  const adapter = session.adapter;
328
- const model = session.config?.fastModelId || session.config?.model;
357
+ const effectiveConfig = resolveDreamSessionConfig(session, sessionId);
358
+ const model = effectiveConfig?.model || effectiveConfig?.primaryModel;
359
+ if (!model) {
360
+ throw new Error(`dream: no session model configured (pass=${pass}, sessionId=${sessionId || 'unknown'})`);
361
+ }
329
362
  if (!adapter || typeof adapter.call !== 'function') {
330
363
  throw new Error(`dream: no adapter.call available (pass=${pass})`);
331
364
  }
@@ -335,7 +368,7 @@ function makeLlm(session) {
335
368
  session._dreamLoopCounter = (session._dreamLoopCounter || 0) + 1;
336
369
  const loopNumber = session._dreamLoopCounter;
337
370
  const turnId = session._dreamTurnId || 'dream';
338
- const effectiveSystem = system || (String(session.config?.language || '').toLowerCase().startsWith('zh')
371
+ const effectiveSystem = system || (String(effectiveConfig?.language || '').toLowerCase().startsWith('zh')
339
372
  ? `你是梦境流水线 — pass: ${pass}。请用中文生成自然语言内容;JSON key 保持英文。`
340
373
  : `You are the dream pipeline — pass: ${pass}.`);
341
374
 
@@ -344,6 +377,7 @@ function makeLlm(session) {
344
377
  system: effectiveSystem,
345
378
  messages: [{ role: 'user', content: prompt }],
346
379
  maxTokens: 2048,
380
+ modelEffort: effectiveConfig?.modelEffort || undefined,
347
381
  });
348
382
 
349
383
  // Emit complete loop event (request + response) to the debug panel.
@@ -366,6 +400,7 @@ function makeLlm(session) {
366
400
  loopNumber,
367
401
  pass,
368
402
  model: model || 'unknown',
403
+ sessionId: sessionId || null,
369
404
  systemPrompt: effectiveSystem,
370
405
  messages: [{ role: 'user', content: prompt }],
371
406
  response: typeof r?.text === 'string' ? r.text : '',
@@ -387,6 +422,15 @@ function makeLlm(session) {
387
422
  }
388
423
 
389
424
 
425
+ function resolveDreamSessionConfig(session, sessionId) {
426
+ const base = session?.config || {};
427
+ if (!sessionId || !session?.yeaftDir) return { ...base };
428
+ const sessionConfig = loadSessionConfig(session.yeaftDir, sessionId);
429
+ if (!sessionConfig || Object.keys(sessionConfig).length === 0) return { ...base };
430
+ return resolveSessionConfig(base, sessionConfig);
431
+ }
432
+
433
+
390
434
  function stampDreamScope(session, evt) {
391
435
  if (!evt || typeof evt !== 'object') return evt;
392
436
  if (evt.sessionId || !session?._dreamActiveGroupId) return evt;
@@ -646,7 +690,7 @@ export async function bootInitEmptyGroups(args) {
646
690
  if (!args || !args.memoryIndex || !args.dreamScheduler) return out;
647
691
  const sessionsRoot = join(args.yeaftDir, 'sessions');
648
692
  let ids;
649
- try { ids = listSessions(sessionsRoot).map(g => g.id); }
693
+ try { ids = listSessionMetas(sessionsRoot).map(g => g.id); }
650
694
  catch { return out; }
651
695
  const empty = [];
652
696
  for (const gid of ids) {
@@ -656,11 +700,7 @@ export async function bootInitEmptyGroups(args) {
656
700
  if (segCount > 0) continue;
657
701
  let hasMessages = false;
658
702
  try {
659
- const h = openSession(sessionsRoot, gid);
660
- // Any message at all is enough — pull the first record off the
661
- // iterator and stop.
662
- const first = h.streamMessages().next();
663
- hasMessages = !first.done;
703
+ hasMessages = loadSessionConversationMessages([sessionsRoot], gid).length > 0;
664
704
  } catch { continue; }
665
705
  if (!hasMessages) continue;
666
706
  empty.push(`sessions/${gid}`);
@@ -716,7 +756,7 @@ export async function bootCatchUpStaleDream(args) {
716
756
  const now = args.now ?? Date.now();
717
757
 
718
758
  let sessionIds;
719
- try { sessionIds = listSessions(sessionsRoot).map(g => g.id); }
759
+ try { sessionIds = listSessionMetas(sessionsRoot).map(g => g.id); }
720
760
  catch { return out; }
721
761
 
722
762
  // Find the newest lastDreamAt across all groups.
@@ -732,9 +772,7 @@ export async function bootCatchUpStaleDream(args) {
732
772
  }
733
773
  if (!anyTraffic) {
734
774
  try {
735
- const h = openSession(sessionsRoot, gid);
736
- const first = h.streamMessages().next();
737
- if (!first.done) anyTraffic = true;
775
+ if (loadSessionConversationMessages([sessionsRoot], gid).length > 0) anyTraffic = true;
738
776
  } catch { /* keep going */ }
739
777
  }
740
778
  }
@@ -187,6 +187,19 @@ export async function writeDreamError(root, scope, info) {
187
187
  }
188
188
  }
189
189
 
190
+ /**
191
+ * Clear a scope's last Dream error after a successful pass. Best-effort: a
192
+ * missing file is fine and I/O failure must not turn success into failure.
193
+ *
194
+ * @param {string} root
195
+ * @param {string} scope
196
+ * @returns {Promise<void>}
197
+ */
198
+ export async function clearDreamError(root, scope) {
199
+ try { await fsp.unlink(join(scopeDirFor(root, scope), ERROR_FILE)); }
200
+ catch { /* best-effort */ }
201
+ }
202
+
190
203
  /**
191
204
  * Read the last dream error JSON for a scope, or null if absent. Used
192
205
  * by the debug panel and by tests. Tolerates a malformed file by
@@ -93,9 +93,7 @@ export function makeSegment(raw) {
93
93
  const tags = Array.isArray(raw.tags)
94
94
  ? raw.tags.map(t => String(t).trim()).filter(Boolean)
95
95
  : [];
96
- const sourceMessages = Array.isArray(raw.sourceMessages)
97
- ? raw.sourceMessages.map(t => String(t).trim()).filter(Boolean)
98
- : [];
96
+ const sourceMessages = normalizeSourceMessages(raw.sourceMessages);
99
97
 
100
98
  const now = new Date().toISOString();
101
99
  const createdAt = raw.createdAt || now;
@@ -105,6 +103,23 @@ export function makeSegment(raw) {
105
103
  return { id, scope, kind, tags, sourceMessages, createdAt, updatedAt, body };
106
104
  }
107
105
 
106
+ function normalizeSourceMessages(value) {
107
+ if (!Array.isArray(value)) return [];
108
+ const out = [];
109
+ for (const item of value) {
110
+ if (typeof item === 'string' || typeof item === 'number') {
111
+ const id = String(item).trim();
112
+ if (id && id !== '[object Object]') out.push(id);
113
+ continue;
114
+ }
115
+ if (item && typeof item === 'object') {
116
+ const id = String(item.id || item.messageId || item.uuid || '').trim();
117
+ if (id) out.push(id);
118
+ }
119
+ }
120
+ return out;
121
+ }
122
+
108
123
  /**
109
124
  * Parse a memory.md text into segments. Tolerant by design:
110
125
  * - Empty / whitespace input → [].