@yeaft/webchat-agent 1.0.82 → 1.0.84

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.82",
3
+ "version": "1.0.84",
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",
@@ -145,7 +145,7 @@ function listConversationSessions(roots) {
145
145
  for (const name of readdirSync(root)) {
146
146
  if (name.startsWith('.')) continue;
147
147
  try {
148
- const dir = sessionConversationDir(root, name);
148
+ const dir = sessionDir(root, name);
149
149
  if (!hasReadableMessages(dir)) continue;
150
150
  out.add(name);
151
151
  } catch {
@@ -158,9 +158,10 @@ function listConversationSessions(roots) {
158
158
  }
159
159
 
160
160
  /**
161
- * Load hot and cold conversation messages for one Yeaft Session. This mirrors
162
- * ConversationStore's session history source: `conversation/cold` plus
163
- * `conversation/messages`, deduped by id and sorted by message sequence.
161
+ * Load persisted conversation messages for one Yeaft Session. Supports both
162
+ * projections currently present on disk:
163
+ * - ConversationStore markdown/jsonl under `conversation/`
164
+ * - Session append-only audit log under `messages/*.jsonl`
164
165
  *
165
166
  * @param {string[]} roots session transcript roots in priority order
166
167
  * @param {string} sessionId
@@ -169,19 +170,26 @@ function listConversationSessions(roots) {
169
170
  function loadSessionConversationMessages(roots, sessionId) {
170
171
  const byId = new Map();
171
172
  for (const root of roots) {
172
- const dir = sessionConversationDir(root, sessionId);
173
- for (const m of loadConversationDirMessages(dir)) {
173
+ const dir = sessionDir(root, sessionId);
174
+ for (const m of loadSessionDiskMessages(dir, sessionId)) {
174
175
  if (!byId.has(m.id)) byId.set(m.id, m);
175
176
  }
176
177
  }
177
178
  return [...byId.values()].sort(compareMessagesBySeq);
178
179
  }
179
180
 
180
- function sessionConversationDir(root, sessionId) {
181
- return join(root, safeDirComponent(sessionId), 'conversation');
181
+ function sessionDir(root, sessionId) {
182
+ return join(root, safeDirComponent(sessionId));
182
183
  }
183
184
 
184
- function hasReadableMessages(conversationDir) {
185
+ function hasReadableMessages(dir) {
186
+ const conversationDir = join(dir, 'conversation');
187
+ if (hasMarkdownMessages(conversationDir)) return true;
188
+ if (hasJsonlMessages(join(conversationDir, 'segments'))) return true;
189
+ return hasJsonlMessages(join(dir, 'messages'));
190
+ }
191
+
192
+ function hasMarkdownMessages(conversationDir) {
185
193
  return ['messages', 'cold'].some(kind => {
186
194
  const dir = join(conversationDir, kind);
187
195
  try {
@@ -192,8 +200,31 @@ function hasReadableMessages(conversationDir) {
192
200
  });
193
201
  }
194
202
 
203
+ function hasJsonlMessages(dir) {
204
+ try {
205
+ return statSync(dir).isDirectory() && readdirSync(dir).some(f => f.endsWith('.jsonl'));
206
+ } catch {
207
+ return false;
208
+ }
209
+ }
210
+
211
+ function loadSessionDiskMessages(dir, sessionId) {
212
+ 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
+ ];
221
+ }
222
+
195
223
  function loadConversationDirMessages(conversationDir) {
196
- return ['cold', 'messages'].flatMap(kind => loadMessageDir(join(conversationDir, kind)));
224
+ return [
225
+ ...['cold', 'messages'].flatMap(kind => loadMessageDir(join(conversationDir, kind))),
226
+ ...loadConversationJsonlMessages(join(conversationDir, 'segments')),
227
+ ];
197
228
  }
198
229
 
199
230
  function loadMessageDir(dir) {
@@ -207,6 +238,73 @@ function loadMessageDir(dir) {
207
238
  .filter(m => m && m.id);
208
239
  }
209
240
 
241
+ function loadConversationJsonlMessages(dir) {
242
+ return loadJsonlDir(dir, normalizeConversationJsonlMessage);
243
+ }
244
+
245
+ function loadSessionJsonlMessages(dir, sessionId) {
246
+ return loadJsonlDir(dir, row => normalizeSessionJsonlMessage(row, sessionId));
247
+ }
248
+
249
+ function loadJsonlDir(dir, normalize) {
250
+ if (!existsSync(dir)) return [];
251
+ let files = [];
252
+ try {
253
+ files = readdirSync(dir).filter(f => f.endsWith('.jsonl')).sort();
254
+ } catch {
255
+ return [];
256
+ }
257
+ const out = [];
258
+ for (const file of files) {
259
+ let raw = '';
260
+ try { raw = readFileSync(join(dir, file), 'utf8'); } catch { continue; }
261
+ for (const line of raw.split('\n')) {
262
+ if (!line) continue;
263
+ try {
264
+ const msg = normalize(JSON.parse(line));
265
+ if (msg && msg.id) out.push(msg);
266
+ } catch {
267
+ // Skip corrupt rows; one bad JSONL line must not make Dream blind.
268
+ }
269
+ }
270
+ }
271
+ return out;
272
+ }
273
+
274
+ function normalizeConversationJsonlMessage(row) {
275
+ if (!row || typeof row !== 'object' || !row.id) return null;
276
+ return {
277
+ id: row.id,
278
+ role: row.role || 'assistant',
279
+ content: stringifyMessageBody(row.content ?? row.text ?? ''),
280
+ time: row.time || row.ts || '',
281
+ sessionId: row.sessionId || null,
282
+ speakerVpId: row.speakerVpId || null,
283
+ };
284
+ }
285
+
286
+ function normalizeSessionJsonlMessage(row, sessionId) {
287
+ if (!row || typeof row !== 'object' || !row.id) return null;
288
+ const role = row.role || (row.from === 'user' ? 'user' : 'assistant');
289
+ const speakerVpId = role === 'assistant'
290
+ ? (row.speakerVpId || row.meta?.senderVpId || (row.from && row.from !== 'user' ? row.from : null))
291
+ : null;
292
+ return {
293
+ id: row.id,
294
+ role,
295
+ content: stringifyMessageBody(row.content ?? row.text ?? ''),
296
+ time: row.time || row.ts || '',
297
+ sessionId,
298
+ speakerVpId,
299
+ };
300
+ }
301
+
302
+ function stringifyMessageBody(value) {
303
+ if (typeof value === 'string') return value;
304
+ if (value == null) return '';
305
+ try { return JSON.stringify(value); } catch { return String(value); }
306
+ }
307
+
210
308
  function compareMessagesBySeq(a, b) {
211
309
  const sa = parseSeqFromId(a?.id);
212
310
  const sb = parseSeqFromId(b?.id);
@@ -1190,6 +1190,12 @@ export function __testGroupHistory(sessionId) {
1190
1190
  return getOrCreateSessionHistory(sessionId);
1191
1191
  }
1192
1192
 
1193
+ export function __testResolveVpEffectiveConfig(sessionId) {
1194
+ if (!session) return null;
1195
+ const sessionConfigRoot = ctx.CONFIG?.yeaftDir || session.yeaftDir;
1196
+ return resolveSessionConfig(session.config, loadSessionConfig(sessionConfigRoot, sessionId));
1197
+ }
1198
+
1193
1199
  /**
1194
1200
  * Test-only: install a minimal `session` so `hydrateGroupHistory` can
1195
1201
  * read from a real `ConversationStore`. Pass `null` to clear.
@@ -1293,11 +1299,13 @@ function getOrCreateVpEngine(sessionId, vpId, threadId = 'main') {
1293
1299
  let eng = vpEngines.get(key);
1294
1300
  if (eng) return eng;
1295
1301
  if (!session) throw new Error('getOrCreateVpEngine: session not loaded');
1296
- // Per-group config overlay (v1: model only). Falls back to the
1297
- // session's user-level config when no override is set. The resolver
1298
- // never mutates session.config it returns a new object.
1299
- const yeaftDir = ctx.CONFIG?.yeaftDir || session.yeaftDir;
1300
- const groupCfg = loadSessionConfig(yeaftDir, sessionId);
1302
+ // Per-session config overlay (v1: model only). Falls back to the
1303
+ // session's user-level config when no override is set. Prefer the agent-local
1304
+ // config root: sessionConfigPath() resolves registered workDir-backed
1305
+ // sessions from there, while still allowing a later agent-local session in
1306
+ // the same bridge runtime to read its own override after a workDir-first boot.
1307
+ const sessionConfigRoot = ctx.CONFIG?.yeaftDir || session.yeaftDir;
1308
+ const groupCfg = loadSessionConfig(sessionConfigRoot, sessionId);
1301
1309
  const effectiveConfig = resolveSessionConfig(session.config, groupCfg);
1302
1310
  eng = new Engine({
1303
1311
  adapter: session.adapter,