@yeaft/webchat-agent 1.0.290 → 1.0.292

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.290",
3
+ "version": "1.0.292",
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
@@ -84,6 +84,8 @@ const AMS_ADJUST_TIMEOUT_MS = 30_000;
84
84
  /** Maximum silence while a visible turn waits for a result-producing task. */
85
85
  const DEFAULT_ASYNC_TASK_WAIT_TIMEOUT_MS = 120_000;
86
86
 
87
+ const DEFAULT_MEMORY_RECALL_LIMIT = 8;
88
+
87
89
  // ─── LLM retry policy defaults ──────────────────────────────────
88
90
  // Hard-coded floor / ceiling for retry behaviour. The engine reads the
89
91
  // effective policy from `config.llmRetry` so users can dial these via
@@ -442,6 +444,47 @@ function isZhRuntimeLanguage(language) {
442
444
  return String(language || '').toLowerCase().startsWith('zh');
443
445
  }
444
446
 
447
+ function resolveMemoryRecallLimit(config) {
448
+ const raw = config?.memoryRecallLimit ?? config?.dreamMemoryRecallLimit;
449
+ if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_MEMORY_RECALL_LIMIT;
450
+ return Math.max(1, Math.floor(raw));
451
+ }
452
+
453
+ function loadedMemoryDebugEntries(snapshot) {
454
+ const snap = snapshot || {};
455
+ return [
456
+ ...loadedResidentDebugEntries(snap.resident || []),
457
+ ...loadedSegmentDebugEntries(snap.recent || [], 'recent'),
458
+ ...loadedSegmentDebugEntries(snap.onDemand || [], 'onDemand'),
459
+ ];
460
+ }
461
+
462
+ function loadedResidentDebugEntries(entries) {
463
+ return (entries || []).map((entry, index) => ({
464
+ id: `resident:${entry.scope || index}`,
465
+ layer: 'resident',
466
+ scope: entry.scope || null,
467
+ label: memoryScopeLabel(entry.scope || ''),
468
+ kind: 'summary',
469
+ score: null,
470
+ tags: [],
471
+ body: entry.summary || '',
472
+ })).filter(entry => entry.body);
473
+ }
474
+
475
+ function loadedSegmentDebugEntries(segments, layer) {
476
+ return (segments || []).map((seg, index) => ({
477
+ id: seg.id || `${layer}:${index}`,
478
+ layer,
479
+ scope: seg.scope || null,
480
+ label: memoryScopeLabel(seg.scope || ''),
481
+ kind: seg.kind || null,
482
+ score: typeof seg.score === 'number' ? seg.score : null,
483
+ tags: Array.isArray(seg.tags) ? seg.tags : [],
484
+ body: seg.body || '',
485
+ })).filter(entry => entry.body);
486
+ }
487
+
445
488
  export class Engine {
446
489
  /** @type {import('./llm/adapter.js').LLMAdapter} */
447
490
  #adapter;
@@ -906,6 +949,7 @@ export class Engine {
906
949
  * ownVpId: string|null,
907
950
  * scopes: string[],
908
951
  * snapshotBlock: string,
952
+ * snapshot: import('./memory/ams.js').AmsSnapshot,
909
953
  * residentEntries: Array<{scope:string, summary:string}>,
910
954
  * } | null}
911
955
  */
@@ -938,14 +982,15 @@ export class Engine {
938
982
  ams.setOnDemand(segs);
939
983
 
940
984
  // (c) Snapshot — render the AMS layers as a single prompt block.
941
- const snapshotBlock = this.#renderAmsSnapshot(ams, this.#config.language || 'en', args.userMsg || '');
985
+ const snapshot = ams.snapshot({ userMsg: args.userMsg || '' });
986
+ const snapshotBlock = this.#renderAmsSnapshot(snapshot, this.#config.language || 'en');
942
987
 
943
988
  const scopes = buildRelevantScopes({
944
989
  sessionId: args.sessionId,
945
990
  vpId: ownVpId,
946
991
  });
947
992
 
948
- return { ams, sessionKey, ownVpId, scopes, snapshotBlock, residentEntries };
993
+ return { ams, sessionKey, ownVpId, scopes, snapshotBlock, snapshot, residentEntries };
949
994
  }
950
995
 
951
996
  /**
@@ -953,13 +998,11 @@ export class Engine {
953
998
  * injection. Mirrors the heading style of the existing memory blocks
954
999
  * so the LLM sees a consistent layout.
955
1000
  *
956
- * @param {import('./memory/ams.js').ActiveMemorySet} ams
1001
+ * @param {import('./memory/ams.js').AmsSnapshot} snap
957
1002
  * @param {string} [language]
958
- * @param {string} [userMsg]
959
1003
  * @returns {string}
960
1004
  */
961
- #renderAmsSnapshot(ams, language = 'en', userMsg = '') {
962
- const snap = ams.snapshot({ userMsg });
1005
+ #renderAmsSnapshot(snap, language = 'en') {
963
1006
  if (!snap) return '';
964
1007
  const parts = [];
965
1008
  if (snap.resident.length === 0 && snap.recent.length === 0 && snap.onDemand.length === 0) {
@@ -1342,7 +1385,7 @@ export class Engine {
1342
1385
  * @returns {Promise<{ profile: string, entries: object[], formatted: string }|null>}
1343
1386
  */
1344
1387
  async #recallMemory(prompt, ctx = {}) {
1345
- const memory = { profile: '', entries: [], formatted: '' };
1388
+ const memory = { profile: '', entries: [], formatted: '', meta: {} };
1346
1389
  if (!this.#memoryIndex) return memory;
1347
1390
  try {
1348
1391
  const result = runMemoryPreflow(this.#memoryIndex, {
@@ -1351,11 +1394,13 @@ export class Engine {
1351
1394
  chatId: ctx.chatId || this.#chatId,
1352
1395
  vpId: ctx.vpId,
1353
1396
  extraScopes: ctx.extraScopes,
1397
+ pickLimit: resolveMemoryRecallLimit(this.#config),
1354
1398
  fallbackOnEmpty: true,
1355
1399
  });
1356
1400
  memory.profile = result.profile || '';
1357
1401
  memory.entries = result.entries || [];
1358
1402
  memory.formatted = result.formatted || '';
1403
+ memory.meta = result.meta || {};
1359
1404
  } catch {
1360
1405
  // Fail soft — empty injection.
1361
1406
  }
@@ -2029,6 +2074,7 @@ export class Engine {
2029
2074
  if (amsContext && amsContext.snapshotBlock) {
2030
2075
  memoryInjection = amsContext.snapshotBlock;
2031
2076
  }
2077
+ const loadedMemoryForDebug = loadedMemoryDebugEntries(amsContext?.snapshot);
2032
2078
 
2033
2079
  // Diagnostic payload for the Dream debug panel. The full AMS Resident
2034
2080
  // layer can include user and per-VP summaries, but the browser-facing
@@ -2282,19 +2328,20 @@ export class Engine {
2282
2328
  yield { type: 'skill_error', turnId: queryTurnId, skillName: explicitSkillName, message: skillResolutionError };
2283
2329
  }
2284
2330
 
2285
- // Surface memory recall to the debug panel right after turn_open.
2286
- // recallResult was loaded above; emit a structured `memory_used`
2287
- // event so the UI can show "loaded N segments" without parsing
2288
- // the legacy `recall` event (which only carried entryCount).
2289
- if (recallResult && Array.isArray(recallResult.entries) && recallResult.entries.length > 0) {
2331
+ // Surface the exact memory that entered the prompt. This must be based on
2332
+ // the AMS snapshot, not raw FTS candidates, otherwise debug can claim memory
2333
+ // was loaded even when prompt cleanup, dedupe, or token budget dropped it.
2334
+ if (loadedMemoryForDebug.length > 0) {
2290
2335
  yield {
2291
2336
  type: 'memory_used',
2292
2337
  turnId: queryTurnId,
2293
- loaded: recallResult.entries.map(e => ({
2294
- id: e && e.id || null,
2295
- score: e && typeof e.score === 'number' ? e.score : null,
2296
- kind: e && e.kind || null,
2297
- })),
2338
+ loaded: loadedMemoryForDebug,
2339
+ meta: {
2340
+ recallLimit: resolveMemoryRecallLimit(this.#config),
2341
+ recallCandidates: Number.isFinite(recallResult?.meta?.hitCount)
2342
+ ? recallResult.meta.hitCount
2343
+ : (recallResult && Array.isArray(recallResult.entries) ? recallResult.entries.length : 0),
2344
+ },
2298
2345
  };
2299
2346
  }
2300
2347
 
@@ -19,6 +19,8 @@ import { extractKeywords } from './keywords.js';
19
19
  import { approxTokens } from './budget.js';
20
20
  import { isVpForeign } from './store.js';
21
21
 
22
+ export const DEFAULT_PICK_LIMIT = 8;
23
+
22
24
  /**
23
25
  * @typedef {object} PreflowOptions
24
26
  * @property {string} userMsg
@@ -27,6 +29,7 @@ import { isVpForeign } from './store.js';
27
29
  * @property {string[]} [currentTags] tags from the current group/feature context
28
30
  * @property {number} [topK] max FTS rows to fetch (default 50)
29
31
  * @property {number} [budgetTokens] onDemand budget (caller-supplied)
32
+ * @property {number} [pickLimit] max picked segments (default 8)
30
33
  */
31
34
 
32
35
  /**
@@ -54,6 +57,8 @@ export function runPreflow(index, opts) {
54
57
  const topK = Number.isFinite(opts.topK) && opts.topK > 0 ? opts.topK : 50;
55
58
  const budgetTokens = Number.isFinite(opts.budgetTokens) && opts.budgetTokens > 0
56
59
  ? opts.budgetTokens : Infinity;
60
+ const pickLimit = Number.isFinite(opts.pickLimit) && opts.pickLimit > 0
61
+ ? Math.floor(opts.pickLimit) : DEFAULT_PICK_LIMIT;
57
62
 
58
63
  const keywords = extractKeywords(userMsg);
59
64
  if (keywords.length === 0) {
@@ -80,7 +85,7 @@ export function runPreflow(index, opts) {
80
85
  let dropped = 0;
81
86
  for (const h of reranked) {
82
87
  const tk = approxTokens(h.body);
83
- if (cost + tk <= budgetTokens) {
88
+ if (picked.length < pickLimit && cost + tk <= budgetTokens) {
84
89
  picked.push(toSegment(h));
85
90
  cost += tk;
86
91
  } else {
@@ -152,7 +157,7 @@ export function rerank(hits, ctx) {
152
157
  return { ...h, _score: score };
153
158
  })
154
159
  .sort((a, b) => a._score - b._score)
155
- .map(({ _score, ...rest }) => rest);
160
+ .map(({ _score, ...rest }) => ({ ...rest, score: _score }));
156
161
  }
157
162
 
158
163
  function toSegment(h) {
@@ -163,6 +168,7 @@ function toSegment(h) {
163
168
  tags: h.tags,
164
169
  sourceMessages: h.sourceMessages,
165
170
  body: h.body,
171
+ score: typeof h.score === 'number' ? h.score : (typeof h.rank === 'number' ? h.rank : undefined),
166
172
  createdAt: h.createdAt,
167
173
  updatedAt: h.updatedAt,
168
174
  };
@@ -259,6 +259,7 @@ export function formatPickedForInjection(picked) {
259
259
  * @property {string[]} [currentTags] Contextual tags for rerank
260
260
  * @property {number} [topK] Max FTS rows fetched (default 50)
261
261
  * @property {number} [budgetTokens] Token budget for picked segments
262
+ * @property {number} [pickLimit] Max picked segments (default 8)
262
263
  * @property {boolean} [fallbackOnEmpty] Include bounded recent scoped segments when FTS has no hits
263
264
  * @property {number} [fallbackPerScope] Max fallback segments per scope
264
265
  */
@@ -353,6 +354,7 @@ export function runMemoryPreflow(index, opts) {
353
354
  currentTags: opts.currentTags || [],
354
355
  topK: opts.topK,
355
356
  budgetTokens: opts.budgetTokens,
357
+ pickLimit: opts.pickLimit,
356
358
  });
357
359
 
358
360
  let fallbackUsed = false;
@@ -362,6 +364,7 @@ export function runMemoryPreflow(index, opts) {
362
364
  ownVpId: opts.vpId || null,
363
365
  budgetTokens: opts.budgetTokens,
364
366
  perScope: opts.fallbackPerScope,
367
+ pickLimit: opts.pickLimit,
365
368
  });
366
369
  if (fallback.length > 0) {
367
370
  fallbackUsed = true;
@@ -400,6 +403,7 @@ function fallbackScopedSegments(index, opts) {
400
403
  const scopes = prioritizeFallbackScopes(filterScopes(opts.relevantScopes || [], opts.ownVpId || null));
401
404
  const perScope = Number.isFinite(opts.perScope) && opts.perScope > 0 ? Math.floor(opts.perScope) : 2;
402
405
  const budgetTokens = Number.isFinite(opts.budgetTokens) && opts.budgetTokens > 0 ? opts.budgetTokens : 1200;
406
+ const pickLimit = Number.isFinite(opts.pickLimit) && opts.pickLimit > 0 ? Math.floor(opts.pickLimit) : 8;
403
407
  const buckets = [];
404
408
  for (const scope of scopes) {
405
409
  let segs = [];
@@ -419,6 +423,7 @@ function fallbackScopedSegments(index, opts) {
419
423
  if (!seg) continue;
420
424
  const tk = approxTokens(seg.body || '');
421
425
  if (tk <= 0 || cost + tk > budgetTokens) continue;
426
+ if (out.length >= pickLimit) return out;
422
427
  out.push(seg);
423
428
  cost += tk;
424
429
  }
@@ -3978,6 +3978,7 @@ function handleEngineEvent(event, hctx) {
3978
3978
  type: 'memory_used',
3979
3979
  turnId: event.turnId,
3980
3980
  loaded: event.loaded || [],
3981
+ meta: event.meta || null,
3981
3982
  }, envelope);
3982
3983
  break;
3983
3984