@yeaft/webchat-agent 1.0.116 → 1.0.117

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.116",
3
+ "version": "1.0.117",
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",
package/yeaft/engine.js CHANGED
@@ -379,7 +379,7 @@ export function shouldAllowGroupReflection({
379
379
  * @param {{
380
380
  * sessionId?: string|null,
381
381
  * ownVpId?: string|null,
382
- * summaries: { user?: string, session?: string, vp?: string }
382
+ * summaries: { user?: string, session?: string, vp?: string, topics?: Array<{scope:string, summary:string}> }
383
383
  * }} args
384
384
  * @returns {Array<{scope: string, summary: string}>}
385
385
  */
@@ -390,6 +390,11 @@ export function buildResidentEntries(args) {
390
390
  if (args.sessionId && summaries.session) {
391
391
  out.push({ scope: `sessions/${args.sessionId}`, summary: summaries.session });
392
392
  }
393
+ if (args.sessionId && Array.isArray(summaries.topics)) {
394
+ for (const topic of summaries.topics) {
395
+ if (topic?.scope && topic?.summary) out.push({ scope: topic.scope, summary: topic.summary });
396
+ }
397
+ }
393
398
  // VP per-session isolation (2026-06-09): the VP summary scope MUST be
394
399
  // session-qualified. The legacy bare `vp/<id>` scope was a structural
395
400
  // (see #loadLayerASummaries, kind:'group-vp'), so labelling it `vp/<id>`
@@ -783,12 +788,13 @@ export class Engine {
783
788
  * dream tick (Phase 6) is what populates these; on a fresh install they
784
789
  * all return ''.
785
790
  *
786
- * @param {{sessionId?: string, vpId?: string, language?: string}} ctx
787
- * @returns {Promise<{user:string, session:string, vp:string}>}
791
+ * @param {{sessionId?: string, vpId?: string, language?: string, topicScopes?: string[]}} ctx
792
+ * @returns {Promise<{user:string, session:string, vp:string, topics:Array<{scope:string, summary:string}>}>}
788
793
  */
789
- async #loadLayerASummaries({ sessionId, vpId, language } = {}) {
790
- if (!this.#yeaftDir) return { user: '', session: '', vp: '' };
794
+ async #loadLayerASummaries({ sessionId, vpId, language, topicScopes } = {}) {
795
+ if (!this.#yeaftDir) return { user: '', session: '', vp: '', topics: [] };
791
796
  const memoryRoot = `${this.#yeaftDir}/memory`;
797
+ const topicScopeList = Array.isArray(topicScopes) ? topicScopes.slice(0, 12) : [];
792
798
  const tasks = [
793
799
  readScopeSummary({ kind: 'user' }, { root: memoryRoot, language }).catch(() => ''),
794
800
  sessionId
@@ -797,17 +803,24 @@ export class Engine {
797
803
  vpId && sessionId
798
804
  ? readScopeSummary({ kind: 'session-vp', sessionId, id: vpId }, { root: memoryRoot, language }).catch(() => '')
799
805
  : Promise.resolve(''),
806
+ Promise.all(topicScopeList.map(scope => readTopicSummary(scope, { root: memoryRoot, language }))),
800
807
  ];
801
- const [user, session, vp] = await Promise.all(tasks);
802
- return { user: user || '', session: session || '', vp: vp || '' };
808
+ const [user, session, vp, topicsRaw] = await Promise.all(tasks);
809
+ const topics = (topicsRaw || []).filter(t => t && t.summary);
810
+ return { user: user || '', session: session || '', vp: vp || '', topics };
803
811
  }
804
812
 
805
813
  async #loadSessionTopicLabels(sessionId, limit = 8) {
814
+ return (await this.#loadSessionTopicScopes(sessionId, limit))
815
+ .map(scope => scope.replace(/^sessions\/[^/]+\/topic\//, ''));
816
+ }
817
+
818
+ async #loadSessionTopicScopes(sessionId, limit = 24) {
806
819
  if (!this.#yeaftDir || !sessionId) return [];
807
820
  const topicRoot = join(this.#yeaftDir, 'memory', 'sessions', sessionId, 'topic');
808
821
  const labels = [];
809
822
  await collectTopicLabels(topicRoot, '', labels, limit).catch(() => {});
810
- return labels;
823
+ return labels.map(label => `sessions/${sessionId}/topic/${label}`);
811
824
  }
812
825
 
813
826
  /**
@@ -1240,7 +1253,7 @@ export class Engine {
1240
1253
  * without injection.
1241
1254
  *
1242
1255
  * @param {string} prompt
1243
- * @param {{ sessionId?: string, vpId?: string }} [ctx]
1256
+ * @param {{ sessionId?: string, vpId?: string, extraScopes?: string[] }} [ctx]
1244
1257
  * @returns {Promise<{ profile: string, entries: object[], formatted: string }|null>}
1245
1258
  */
1246
1259
  async #recallMemory(prompt, ctx = {}) {
@@ -1252,6 +1265,8 @@ export class Engine {
1252
1265
  sessionId: ctx.sessionId,
1253
1266
  chatId: ctx.chatId || this.#chatId,
1254
1267
  vpId: ctx.vpId,
1268
+ extraScopes: ctx.extraScopes,
1269
+ fallbackOnEmpty: true,
1255
1270
  });
1256
1271
  memory.profile = result.profile || '';
1257
1272
  memory.entries = result.entries || [];
@@ -1784,11 +1799,13 @@ export class Engine {
1784
1799
  let memoryInjection = '';
1785
1800
  let recallEntryCount = 0;
1786
1801
 
1802
+ const topicScopesForMemory = await this.#loadSessionTopicScopes(sessionId);
1787
1803
  const recallResult = await this.#recallMemory(prompt, {
1788
1804
  sessionId,
1789
1805
  vpId: vpPersona && typeof vpPersona === 'object' && typeof vpPersona.vpId === 'string'
1790
1806
  ? vpPersona.vpId
1791
1807
  : (typeof senderVpId === 'string' ? senderVpId : undefined),
1808
+ extraScopes: topicScopesForMemory,
1792
1809
  });
1793
1810
  recallEntryCount = recallResult && Array.isArray(recallResult.entries)
1794
1811
  ? recallResult.entries.length
@@ -1806,6 +1823,7 @@ export class Engine {
1806
1823
  ? vpPersona.vpId
1807
1824
  : (typeof senderVpId === 'string' ? senderVpId : undefined),
1808
1825
  language: this.#config.language || 'en',
1826
+ topicScopes: topicScopesForMemory,
1809
1827
  });
1810
1828
 
1811
1829
  // ─── AMS: populate + snapshot ───────────────────────────────
@@ -1837,9 +1855,13 @@ export class Engine {
1837
1855
  // frontend state. The full system prompt remains visible in the existing
1838
1856
  // debug-only system-prompt panel.
1839
1857
  const activeGroupDreamScope = sessionId ? `sessions/${sessionId}` : null;
1858
+ const activeTopicDreamPrefix = sessionId ? `sessions/${sessionId}/topic/` : null;
1840
1859
  const dreamResidentLoaded = amsContext && Array.isArray(amsContext.residentEntries)
1841
1860
  ? amsContext.residentEntries
1842
- .filter(e => e && e.scope === activeGroupDreamScope && e.summary)
1861
+ .filter(e => e && e.summary && (
1862
+ e.scope === activeGroupDreamScope
1863
+ || (activeTopicDreamPrefix && e.scope?.startsWith(activeTopicDreamPrefix))
1864
+ ))
1843
1865
  .map(e => ({
1844
1866
  scope: e.scope,
1845
1867
  summary: String(e.summary).slice(0, 4000),
@@ -1854,7 +1876,7 @@ export class Engine {
1854
1876
  // only IDs + tiny labels. (Feature scope retired 2026-05-13.)
1855
1877
  const activeSessionTopics = Array.isArray(sessionTopics)
1856
1878
  ? sessionTopics
1857
- : await this.#loadSessionTopicLabels(sessionId);
1879
+ : topicScopesForMemory.slice(0, 8).map(scope => scope.replace(/^sessions\/[^/]+\/topic\//, ''));
1858
1880
  const activeScope = {
1859
1881
  sessionId: sessionId || '',
1860
1882
  sessionMember: ownVpIdForAms || '',
@@ -3738,6 +3760,18 @@ export class Engine {
3738
3760
  }
3739
3761
  }
3740
3762
 
3763
+ async function readTopicSummary(scope, opts) {
3764
+ const m = /^sessions\/([^/]+)\/topic\/(.+)$/.exec(String(scope || ''));
3765
+ if (!m) return null;
3766
+ const path = m[2].split('/').filter(Boolean);
3767
+ if (path.length === 0) return null;
3768
+ const summary = await readScopeSummary(
3769
+ { kind: 'session-topic', sessionId: m[1], path },
3770
+ opts,
3771
+ ).catch(() => '');
3772
+ return summary ? { scope, summary } : null;
3773
+ }
3774
+
3741
3775
  async function collectTopicLabels(dir, prefix, labels, limit) {
3742
3776
  if (labels.length >= limit) return;
3743
3777
  let entries;
@@ -240,7 +240,7 @@ export function isValidTopic(scope) {
240
240
  */
241
241
  export function isVpForeign(relPath, currentVpId) {
242
242
  if (!relPath || !currentVpId) return false;
243
- const m = /^(?:group|chat|session)\/[^/]+\/vp\/([^/]+)(?:\/|$)/.exec(relPath);
243
+ const m = /^(?:group|chat|sessions?)\/[^/]+\/vp\/([^/]+)(?:\/|$)/.exec(relPath);
244
244
  if (!m) return false;
245
245
  return m[1] !== currentVpId;
246
246
  }
@@ -22,7 +22,8 @@
22
22
  * {profile, entries, formatted} shape the engine already consumes.
23
23
  */
24
24
 
25
- import { runPreflow as runFtsPreflow } from '../memory/preflow.js';
25
+ import { runPreflow as runFtsPreflow, filterScopes } from '../memory/preflow.js';
26
+ import { approxTokens } from '../memory/budget.js';
26
27
  import { resolveFallbackVp, resolveMemberId } from './roster.js';
27
28
 
28
29
  /** Matches `@vp-id` where id is [A-Za-z0-9_-]+. Captures the id. */
@@ -179,6 +180,8 @@ function scopeHeading(scope) {
179
180
  if (m) return `## Memory: Session ${m[1]} (user)`;
180
181
  m = /^session\/([^/]+)\/feature\/(.+)$/.exec(scope);
181
182
  if (m) return `## Memory: Feature ${m[2]}`;
183
+ m = /^sessions\/([^/]+)\/topic\/(.+)$/.exec(scope);
184
+ if (m) return `## Memory: Topic ${m[2]}`;
182
185
  m = /^session\/([^/]+)\/topic\/(.+)$/.exec(scope);
183
186
  if (m) return `## Memory: Topic ${m[2]}`;
184
187
  // Legacy nested group scopes (un-migrated data).
@@ -239,6 +242,8 @@ export function formatPickedForInjection(picked) {
239
242
  * @property {string[]} [currentTags] Contextual tags for rerank
240
243
  * @property {number} [topK] Max FTS rows fetched (default 50)
241
244
  * @property {number} [budgetTokens] Token budget for picked segments
245
+ * @property {boolean} [fallbackOnEmpty] Include bounded recent scoped segments when FTS has no hits
246
+ * @property {number} [fallbackPerScope] Max fallback segments per scope
242
247
  */
243
248
 
244
249
  /**
@@ -282,14 +287,17 @@ export function buildRelevantScopes({ sessionId, chatId, vpId, extra } = {}) {
282
287
  scopes.push(`group/${sessionId}/vp/${vpId}`);
283
288
  }
284
289
  }
285
- if (Array.isArray(extra)) {
286
- for (const s of extra) {
287
- if (s && !scopes.includes(s)) scopes.push(s);
288
- }
289
- }
290
+ appendUniqueScopes(scopes, extra);
290
291
  return scopes;
291
292
  }
292
293
 
294
+ function appendUniqueScopes(scopes, extra) {
295
+ if (!Array.isArray(extra)) return;
296
+ for (const s of extra) {
297
+ if (s && !scopes.includes(s)) scopes.push(s);
298
+ }
299
+ }
300
+
293
301
  /**
294
302
  * Run memory pre-flow for one VP turn. Thin wrapper around
295
303
  * `memory/preflow.js::runPreflow` that:
@@ -321,7 +329,7 @@ export function runMemoryPreflow(index, opts) {
321
329
  extra: opts.extraScopes,
322
330
  });
323
331
 
324
- const result = runFtsPreflow(index, {
332
+ let result = runFtsPreflow(index, {
325
333
  userMsg,
326
334
  relevantScopes,
327
335
  ownVpId: opts.vpId || null,
@@ -330,6 +338,25 @@ export function runMemoryPreflow(index, opts) {
330
338
  budgetTokens: opts.budgetTokens,
331
339
  });
332
340
 
341
+ let fallbackUsed = false;
342
+ if ((result.picked || []).length === 0 && opts.fallbackOnEmpty) {
343
+ const fallback = fallbackScopedSegments(index, {
344
+ relevantScopes,
345
+ ownVpId: opts.vpId || null,
346
+ budgetTokens: opts.budgetTokens,
347
+ perScope: opts.fallbackPerScope,
348
+ });
349
+ if (fallback.length > 0) {
350
+ fallbackUsed = true;
351
+ result = {
352
+ ...result,
353
+ picked: fallback,
354
+ pickedTokens: estimatePickedTokens(fallback),
355
+ droppedCount: result.droppedCount || 0,
356
+ };
357
+ }
358
+ }
359
+
333
360
  // Best-effort profile: pick any user-scope segment body.
334
361
  const userSeg = (result.picked || []).find(p => p.scope === 'user');
335
362
  const profile = userSeg ? (userSeg.body || '').trim() : '';
@@ -346,6 +373,61 @@ export function runMemoryPreflow(index, opts) {
346
373
  pickedTokens: result.pickedTokens,
347
374
  droppedCount: result.droppedCount,
348
375
  hitCount: (result.hits || []).length,
376
+ fallbackUsed,
349
377
  },
350
378
  };
351
379
  }
380
+
381
+ function fallbackScopedSegments(index, opts) {
382
+ if (!index || typeof index.listByScope !== 'function') return [];
383
+ const scopes = prioritizeFallbackScopes(filterScopes(opts.relevantScopes || [], opts.ownVpId || null));
384
+ const perScope = Number.isFinite(opts.perScope) && opts.perScope > 0 ? Math.floor(opts.perScope) : 2;
385
+ const budgetTokens = Number.isFinite(opts.budgetTokens) && opts.budgetTokens > 0 ? opts.budgetTokens : 1200;
386
+ const buckets = [];
387
+ for (const scope of scopes) {
388
+ let segs = [];
389
+ try { segs = index.listByScope(scope) || []; } catch { continue; }
390
+ segs = [...segs]
391
+ .filter(seg => (seg?.body || '').trim())
392
+ .sort(compareSegmentRecency)
393
+ .slice(0, perScope);
394
+ if (segs.length > 0) buckets.push(segs);
395
+ }
396
+
397
+ const out = [];
398
+ let cost = 0;
399
+ for (let i = 0; i < perScope; i += 1) {
400
+ for (const bucket of buckets) {
401
+ const seg = bucket[i];
402
+ if (!seg) continue;
403
+ const tk = approxTokens(seg.body || '');
404
+ if (tk <= 0 || cost + tk > budgetTokens) continue;
405
+ out.push(seg);
406
+ cost += tk;
407
+ }
408
+ }
409
+ return out;
410
+ }
411
+
412
+ function prioritizeFallbackScopes(scopes) {
413
+ const topic = [];
414
+ const rest = [];
415
+ for (const scope of scopes) {
416
+ if (/^(?:sessions|session|group)\/[^/]+\/topic\//.test(scope)) topic.push(scope);
417
+ else rest.push(scope);
418
+ }
419
+ return [...topic, ...rest];
420
+ }
421
+
422
+ function compareSegmentRecency(a, b) {
423
+ return timestampOf(b) - timestampOf(a);
424
+ }
425
+
426
+ function timestampOf(seg) {
427
+ const t = Date.parse(seg?.updatedAt || seg?.createdAt || '');
428
+ return Number.isFinite(t) ? t : 0;
429
+ }
430
+
431
+ function estimatePickedTokens(segments) {
432
+ return (segments || []).reduce((sum, seg) => sum + approxTokens(seg?.body || ''), 0);
433
+ }