@yeaft/webchat-agent 1.0.326 → 1.0.328

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.326",
3
+ "version": "1.0.328",
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
@@ -31,6 +31,7 @@ import { evaluateCompactTriggers } from './compact/triggers.js';
31
31
  import { archiveTurn } from './archive/turn-archive.js';
32
32
  import { archiveToolResults } from './archive/tool-results.js';
33
33
  import { readSummary as readScopeSummary } from './memory/store.js';
34
+ import { ActiveMemorySet } from './memory/ams.js';
34
35
  import { runAdjust } from './memory/adjust.js';
35
36
  import { cleanMemoryPromptText, isMemoryPromptRelevant } from './memory/prompt-cleanup.js';
36
37
  import { isVpSeedBackfillStub } from './memory/seed-backfill.js';
@@ -44,7 +45,7 @@ import { lookupModelLimitSync } from './llm/models-dev.js';
44
45
  import { countTurns } from './turn-utils.js';
45
46
  import { attachRouterPlan, extractPriorPlan, stripMetaForWire } from './router/continuity.js';
46
47
  import { resolveThinking } from './router/thinking.js';
47
- import { approxTokens } from './memory/budget.js';
48
+ import { approxTokens, computeBudget } from './memory/budget.js';
48
49
  import { COLLAB_TOOL_POLICY, isToolErrorOutput, normalizeToolOutput, truncateToolResultIfNeeded } from './tools/registry.js';
49
50
  import { extractDisplayImages, stripDisplayImageData } from './image-assets.js';
50
51
  import { acknowledgePendingNotifications, formatNotificationsForPrompt, peekPendingNotifications } from './sub-agent/notifications.js';
@@ -399,7 +400,7 @@ export function shouldAllowGroupReflection({
399
400
  * @param {{
400
401
  * sessionId?: string|null,
401
402
  * ownVpId?: string|null,
402
- * summaries: { user?: string, session?: string, vp?: string, topics?: Array<{scope:string, summary:string}> }
403
+ * summaries: { user?: string, session?: string, vp?: string, topics?: Array<{scope:string, summary:string}>, relatedSessions?: Array<{sessionId:string, summary:string}> }
403
404
  * }} args
404
405
  * @returns {Array<{scope: string, summary: string}>}
405
406
  */
@@ -445,6 +446,18 @@ export function buildResidentEntries(args) {
445
446
  if (args.sessionId && args.ownVpId && vpSummary && !isVpSeedBackfillStub(vpSummary)) {
446
447
  out.push({ scope: `sessions/${args.sessionId}/vp/${args.ownVpId}`, summary: vpSummary });
447
448
  }
449
+ // Related Session experience is useful but lower-priority than every memory
450
+ // source owned by the active Session/VP. Append it last so the resident
451
+ // budget can never evict current context in favour of historical prose.
452
+ if (Array.isArray(summaries.relatedSessions)) {
453
+ for (const related of summaries.relatedSessions) {
454
+ const relatedSessionId = typeof related?.sessionId === 'string' ? related.sessionId.trim() : '';
455
+ const summary = cleanMemoryPromptText(related?.summary);
456
+ if (relatedSessionId && relatedSessionId !== args.sessionId && summary) {
457
+ out.push({ scope: `sessions/${relatedSessionId}`, summary });
458
+ }
459
+ }
460
+ }
448
461
  return out;
449
462
  }
450
463
 
@@ -452,6 +465,11 @@ function isZhRuntimeLanguage(language) {
452
465
  return String(language || '').toLowerCase().startsWith('zh');
453
466
  }
454
467
 
468
+ function sessionIdFromMemoryScope(scope) {
469
+ const match = /^(?:sessions|session|group)\/([^/]+)$/.exec(String(scope || ''));
470
+ return match ? match[1] : null;
471
+ }
472
+
455
473
  function resolveMemoryRecallLimit(config) {
456
474
  const raw = config?.memoryRecallLimit ?? config?.dreamMemoryRecallLimit;
457
475
  if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_MEMORY_RECALL_LIMIT;
@@ -476,6 +494,7 @@ function loadedResidentDebugEntries(entries) {
476
494
  kind: 'summary',
477
495
  score: null,
478
496
  tags: [],
497
+ category: entry.category || null,
479
498
  body: entry.summary || '',
480
499
  })).filter(entry => entry.body);
481
500
  }
@@ -910,13 +929,17 @@ export class Engine {
910
929
  * dream tick (Phase 6) is what populates these; on a fresh install they
911
930
  * all return ''.
912
931
  *
913
- * @param {{sessionId?: string, vpId?: string, language?: string, topicScopes?: string[]}} ctx
914
- * @returns {Promise<{user:string, session:string, vp:string, topics:Array<{scope:string, summary:string}>}>}
932
+ * @param {{sessionId?: string, vpId?: string, language?: string, topicScopes?: string[], relatedSessionIds?: string[]}} ctx
933
+ * @returns {Promise<{user:string, session:string, vp:string, topics:Array<{scope:string, summary:string}>, relatedSessions:Array<{sessionId:string, summary:string}>}>}
915
934
  */
916
- async #loadLayerASummaries({ sessionId, vpId, language, topicScopes } = {}) {
917
- if (!this.#yeaftDir) return { user: '', session: '', vp: '', topics: [] };
935
+ async #loadLayerASummaries({ sessionId, vpId, language, topicScopes, relatedSessionIds } = {}) {
936
+ if (!this.#yeaftDir) return { user: '', session: '', vp: '', topics: [], relatedSessions: [] };
918
937
  const memoryRoot = `${this.#yeaftDir}/memory`;
919
938
  const topicScopeList = Array.isArray(topicScopes) ? topicScopes.slice(0, 12) : [];
939
+ const relatedIds = Array.from(new Set((Array.isArray(relatedSessionIds) ? relatedSessionIds : [])
940
+ .filter(id => typeof id === 'string' && id.trim() && id.trim() !== sessionId)
941
+ .map(id => id.trim())))
942
+ .slice(0, 8);
920
943
  const tasks = [
921
944
  readScopeSummary({ kind: 'user' }, { root: memoryRoot, language }).catch(() => ''),
922
945
  sessionId
@@ -926,10 +949,18 @@ export class Engine {
926
949
  ? readScopeSummary({ kind: 'session-vp', sessionId, id: vpId }, { root: memoryRoot, language }).catch(() => '')
927
950
  : Promise.resolve(''),
928
951
  Promise.all(topicScopeList.map(scope => readTopicSummary(scope, { root: memoryRoot, language }))),
952
+ Promise.all(relatedIds.map(async relatedSessionId => ({
953
+ sessionId: relatedSessionId,
954
+ summary: await readScopeSummary(
955
+ { kind: 'session', id: relatedSessionId },
956
+ { root: memoryRoot, language },
957
+ ).catch(() => ''),
958
+ }))),
929
959
  ];
930
- const [user, session, vp, topicsRaw] = await Promise.all(tasks);
960
+ const [user, session, vp, topicsRaw, relatedSessionsRaw] = await Promise.all(tasks);
931
961
  const topics = (topicsRaw || []).filter(t => t && t.summary);
932
- return { user: user || '', session: session || '', vp: vp || '', topics };
962
+ const relatedSessions = (relatedSessionsRaw || []).filter(entry => entry && entry.summary);
963
+ return { user: user || '', session: session || '', vp: vp || '', topics, relatedSessions };
933
964
  }
934
965
 
935
966
  async #loadSessionTopicLabels(sessionId, limit = 8) {
@@ -967,27 +998,42 @@ export class Engine {
967
998
  * } | null}
968
999
  */
969
1000
  #prepareAms(args) {
970
- if (!this.#amsRegistry) return null;
971
1001
  const sessionKey = args.sessionId || 'default';
972
1002
  const ownVpId = args.ownVpId || null;
973
- const ams = this.#amsRegistry.getOrCreate(sessionKey, { ownVpId });
1003
+ // Some read-only Engine entry points (notably sub-agents) intentionally do
1004
+ // not own the parent's persistent registry. They still need the single AMS
1005
+ // render outlet, otherwise FTS recall succeeds and then vanishes before the
1006
+ // prompt. Use an isolated per-query AMS in that case: it preserves budgets,
1007
+ // cleanup, and dedupe without sharing mutable parent state or writing disk.
1008
+ const ams = this.#amsRegistry
1009
+ ? this.#amsRegistry.getOrCreate(sessionKey, { ownVpId })
1010
+ : new ActiveMemorySet({
1011
+ ownVpId,
1012
+ budget: computeBudget(this.#config?.maxContextTokens),
1013
+ });
974
1014
 
975
1015
  // Prime #adjustRanBySession from disk-hydrated state on first access:
976
1016
  // a reactivated group resumes with whatever adjustRanThisSession bit
977
1017
  // it had on disconnect, so we don't burn a fresh adjust on every
978
1018
  // reload. Once set true in this session we never clear it.
979
- if (!this.#adjustRanBySession.has(sessionKey)
1019
+ if (this.#amsRegistry
1020
+ && !this.#adjustRanBySession.has(sessionKey)
980
1021
  && this.#amsRegistry.adjustRanThisSession(sessionKey)) {
981
1022
  this.#adjustRanBySession.set(sessionKey, true);
982
1023
  }
983
1024
 
984
1025
  // (a) Resident: rebuild from the same scope summaries the worker
985
1026
  // prompt is already going to see.
1027
+ const relatedSessionIds = new Set((args.summaries?.relatedSessions || [])
1028
+ .map(entry => entry?.sessionId)
1029
+ .filter(Boolean));
986
1030
  const residentEntries = buildResidentEntries({
987
1031
  sessionId: args.sessionId,
988
1032
  ownVpId,
989
1033
  summaries: args.summaries || {},
990
- });
1034
+ }).map(entry => relatedSessionIds.has(entry.scope.replace(/^sessions\//, ''))
1035
+ ? { ...entry, category: 'experience' }
1036
+ : { ...entry, category: 'memory' });
991
1037
  ams.setResident(residentEntries);
992
1038
 
993
1039
  // (b) onDemand: replace with this turn's FTS hits.
@@ -996,7 +1042,11 @@ export class Engine {
996
1042
 
997
1043
  // (c) Snapshot — render the AMS layers as a single prompt block.
998
1044
  const snapshot = ams.snapshot({ userMsg: args.userMsg || '' });
999
- const snapshotBlock = this.#renderAmsSnapshot(snapshot, this.#config.language || 'en');
1045
+ const snapshotBlock = this.#renderAmsSnapshot(
1046
+ snapshot,
1047
+ this.#config.language || 'en',
1048
+ args.sessionId || null,
1049
+ );
1000
1050
 
1001
1051
  const scopes = buildRelevantScopes({
1002
1052
  sessionId: args.sessionId,
@@ -1015,33 +1065,36 @@ export class Engine {
1015
1065
  * @param {string} [language]
1016
1066
  * @returns {string}
1017
1067
  */
1018
- #renderAmsSnapshot(snap, language = 'en') {
1068
+ #renderAmsSnapshot(snap, language = 'en', activeSessionId = null) {
1019
1069
  if (!snap) return '';
1020
1070
  const parts = [];
1021
1071
  if (snap.resident.length === 0 && snap.recent.length === 0 && snap.onDemand.length === 0) {
1022
1072
  return '';
1023
1073
  }
1024
1074
  const zh = isZhRuntimeLanguage(language);
1025
- parts.push(zh ? '## 活跃记忆集' : '## Active Memory Set');
1075
+ const experiences = snap.resident.filter(entry => entry.category === 'experience');
1076
+ const residentMemory = snap.resident.filter(entry => entry.category !== 'experience');
1077
+ parts.push(zh ? '## 相关上下文' : '## Relevant Context');
1026
1078
  parts.push(zh
1027
- ? '以下记忆按当前用户语言呈现;如果个别历史摘要仍是其他语言,请只把它当作事实来源,回答和新增记忆应使用中文。'
1028
- : 'Memory is presented for the current user language; if an older summary is in another language, treat it as factual context and continue in English.');
1029
- if (snap.resident.length > 0) {
1030
- parts.push(zh ? '### 常驻记忆' : '### Resident');
1031
- for (const r of snap.resident) {
1032
- parts.push(`- **${memoryScopeLabel(r.scope)}**: ${r.summary}`);
1079
+ ? '以下内容来自持久记忆与相关 Session 的只读经验总结;只把它当作事实背景,不要把过期执行状态当成当前任务。'
1080
+ : 'The following text comes from persistent memory and read-only summaries of related Sessions. Treat it as factual context, not as current execution state.');
1081
+ if (experiences.length > 0) {
1082
+ parts.push(zh ? '### 过去 Session 的经验总结' : '### Experience From Past Sessions');
1083
+ for (const entry of experiences) {
1084
+ const sourceSessionId = sessionIdFromMemoryScope(entry.scope);
1085
+ const label = sourceSessionId && sourceSessionId !== activeSessionId
1086
+ ? sourceSessionId
1087
+ : memoryScopeLabel(entry.scope);
1088
+ parts.push(`- **${label}**: ${entry.summary}`);
1033
1089
  }
1034
1090
  }
1035
- if (snap.recent.length > 0) {
1036
- parts.push(zh ? '### 最近记忆' : '### Recent');
1037
- for (const s of snap.recent) {
1038
- parts.push(`- (${memoryScopeLabel(s.scope)}) ${(s.body || '').trim()}`);
1091
+ if (residentMemory.length > 0 || snap.recent.length > 0 || snap.onDemand.length > 0) {
1092
+ parts.push(zh ? '### 相关记忆' : '### Relevant Memory');
1093
+ for (const entry of residentMemory) {
1094
+ parts.push(`- **${memoryScopeLabel(entry.scope)}**: ${entry.summary}`);
1039
1095
  }
1040
- }
1041
- if (snap.onDemand.length > 0) {
1042
- parts.push(zh ? '### 按需记忆' : '### OnDemand');
1043
- for (const s of snap.onDemand) {
1044
- parts.push(`- (${memoryScopeLabel(s.scope)}) ${(s.body || '').trim()}`);
1096
+ for (const segment of [...snap.recent, ...snap.onDemand]) {
1097
+ parts.push(`- (${memoryScopeLabel(segment.scope)}) ${(segment.body || '').trim()}`);
1045
1098
  }
1046
1099
  }
1047
1100
  return parts.join('\n');
@@ -2146,6 +2199,7 @@ export class Engine {
2146
2199
  : (typeof senderVpId === 'string' ? senderVpId : undefined),
2147
2200
  language: this.#config.language || 'en',
2148
2201
  topicScopes: topicScopesForResident,
2202
+ relatedSessionIds: projectSessionIds,
2149
2203
  });
2150
2204
 
2151
2205
  // ─── AMS: populate + snapshot ───────────────────────────────
@@ -2211,7 +2265,9 @@ export class Engine {
2211
2265
 
2212
2266
  const projectDoc = this.#getProjectDocBlock(workDir);
2213
2267
  const activeTasks = this.#taskManager
2214
- ? this.#taskManager.renderActiveTasksForPrompt(runtimeSessionId)
2268
+ ? this.#taskManager.renderActiveTasksForPrompt(runtimeSessionId, {
2269
+ language: this.#config.language || 'en',
2270
+ })
2215
2271
  : '';
2216
2272
  let resolvedSkillContent = '';
2217
2273
  let resolvedSkills = [];
@@ -29,14 +29,14 @@ const RECENT_DEFAULT_CAPACITY = 64;
29
29
 
30
30
  /**
31
31
  * @typedef {object} AmsLayers
32
- * @property {Map<string, string>} resident
32
+ * @property {Map<string, { summary: string, category?: string }>} resident
33
33
  * @property {Array<{ id: string, seg: import('./segment.js').Segment, ts: number }>} recent
34
34
  * @property {Map<string, import('./segment.js').Segment>} onDemand
35
35
  */
36
36
 
37
37
  /**
38
38
  * @typedef {object} AmsSnapshot
39
- * @property {Array<{ scope: string, summary: string }>} resident
39
+ * @property {Array<{ scope: string, summary: string, category?: string }>} resident
40
40
  * @property {import('./segment.js').Segment[]} recent
41
41
  * @property {import('./segment.js').Segment[]} onDemand
42
42
  * @property {{ resident: number, recent: number, onDemand: number, total: number }} usage
@@ -55,8 +55,8 @@ export class ActiveMemorySet {
55
55
  this.ownVpId = opts.ownVpId || null;
56
56
  this.budget = opts.budget;
57
57
  this.recentCapacity = opts.recentCapacity || RECENT_DEFAULT_CAPACITY;
58
- /** @type {Map<string, string>} */
59
- this._resident = new Map(); // scope → summaryText
58
+ /** @type {Map<string, { summary: string, category?: string }>} */
59
+ this._resident = new Map(); // scope → prompt-facing summary metadata
60
60
  /** @type {Map<string, { seg: import('./segment.js').Segment, ts: number }>} */
61
61
  this._recent = new Map(); // segId → entry (insertion-order is LRU order)
62
62
  /** @type {Map<string, import('./segment.js').Segment>} */
@@ -69,7 +69,7 @@ export class ActiveMemorySet {
69
69
  * Replace the resident layer with a fresh set of scope→summary
70
70
  * pairs. Foreign VP scopes are silently dropped.
71
71
  *
72
- * @param {Array<{ scope: string, summary: string }>} entries
72
+ * @param {Array<{ scope: string, summary: string, category?: string }>} entries
73
73
  */
74
74
  setResident(entries) {
75
75
  this._resident.clear();
@@ -77,7 +77,10 @@ export class ActiveMemorySet {
77
77
  if (this._isForeignVp(e.scope)) continue;
78
78
  const summary = cleanMemoryPromptText(e.summary);
79
79
  if (!summary) continue;
80
- this._resident.set(e.scope, summary);
80
+ this._resident.set(e.scope, {
81
+ summary,
82
+ ...(typeof e.category === 'string' && e.category ? { category: e.category } : {}),
83
+ });
81
84
  }
82
85
  }
83
86
 
@@ -159,8 +162,15 @@ export class ActiveMemorySet {
159
162
  // Resident: pack scopes by priority order (caller provides via insert
160
163
  // order — current group's own vp first, then user, etc.).
161
164
  const { picked: resPicked, cost: resCost } = pickMemoryItems({
162
- items: [...this._resident.entries()].map(([scope, summary]) => ({
163
- scope, summary: filterMemoryPromptTextForPrompt(summary, userMsg),
165
+ items: [...this._resident.entries()].map(([scope, entry]) => ({
166
+ scope,
167
+ // Related-Session summaries are explicitly historical context. Keep the
168
+ // bounded prose intact and label it as experience instead of dropping
169
+ // the whole paragraph because it mentions an old PR/tag/task state.
170
+ summary: entry.category === 'experience'
171
+ ? cleanMemoryPromptText(entry.summary)
172
+ : filterMemoryPromptTextForPrompt(entry.summary, userMsg),
173
+ ...(entry.category ? { category: entry.category } : {}),
164
174
  })),
165
175
  budget: this.budget.resident,
166
176
  seen: seenPromptText,
@@ -20,6 +20,7 @@ import { getRuntimePlatformInfo } from '../runtime-platform.js';
20
20
  const LOG_PREVIEW_BYTES = 4096;
21
21
  const SUB_AGENT_LOG_PREVIEW_BYTES = 1024 * 1024;
22
22
  const DEFAULT_CANCEL_ESCALATION_MS = 2000;
23
+ const PROMPT_TASK_LIMIT = 5;
23
24
 
24
25
  function logPreviewBytesFor(task) {
25
26
  return task?.kind === 'sub_agent' ? SUB_AGENT_LOG_PREVIEW_BYTES : LOG_PREVIEW_BYTES;
@@ -54,9 +55,35 @@ function publicSnapshot(task) {
54
55
  };
55
56
  }
56
57
 
57
- function taskCommand(task) {
58
- const command = task?.runtime?.command;
59
- return typeof command === 'string' && command.trim() ? command.trim() : '';
58
+ function taskKindLabel(kind, language) {
59
+ const zh = String(language || '').toLowerCase().startsWith('zh');
60
+ if (kind === 'sub_agent') return zh ? '子 Agent' : 'sub-agent';
61
+ if (kind === 'shell') return zh ? '后台命令' : 'background command';
62
+ return zh ? '后台任务' : 'background task';
63
+ }
64
+
65
+ function safeTaskName(task) {
66
+ const name = typeof task?.runtime?.name === 'string' ? task.runtime.name.trim() : '';
67
+ return /^[\p{L}\p{N}][\p{L}\p{N}._-]{0,63}$/u.test(name) ? name : '';
68
+ }
69
+
70
+ function promptTaskLabel(task, language) {
71
+ const name = task?.kind === 'sub_agent' ? safeTaskName(task) : '';
72
+ if (!name) return taskKindLabel(task?.kind, language);
73
+ return String(language || '').toLowerCase().startsWith('zh')
74
+ ? `子 Agent ${name}`
75
+ : `sub-agent ${name}`;
76
+ }
77
+
78
+ function taskStatusLabel(status, language) {
79
+ const zh = String(language || '').toLowerCase().startsWith('zh');
80
+ if (!zh) return String(status || 'running').replace(/_/g, ' ');
81
+ const labels = {
82
+ running: '运行中',
83
+ queued: '等待中',
84
+ cancelling: '正在取消',
85
+ };
86
+ return labels[status] || String(status || '运行中').replace(/_/g, ' ');
60
87
  }
61
88
 
62
89
  export class TaskManager {
@@ -358,17 +385,27 @@ export class TaskManager {
358
385
  return publicSnapshot(task);
359
386
  }
360
387
 
361
- renderActiveTasksForPrompt(sessionId = null) {
388
+ renderActiveTasksForPrompt(sessionId = null, { language = 'en', limit = PROMPT_TASK_LIMIT } = {}) {
362
389
  const tasks = this.listActiveTasks(sessionId);
363
390
  if (tasks.length === 0) return '';
364
- const lines = ['<active_tasks>'];
365
- for (const task of tasks) {
366
- const preview = (task.log?.preview || '').trim().split('\n').slice(-3).join(' | ');
367
- const command = taskCommand(task);
368
- const cancelRequestedAt = typeof task.runtime?.cancelRequestedAt === 'string' ? task.runtime.cancelRequestedAt : '';
369
- lines.push(`- ${task.id} | ${task.kind} | ${task.status} | owner=${task.ownerVpId || 'unknown'} | title=${JSON.stringify(task.title)}${command ? ` | command=${JSON.stringify(command)}` : ''}${cancelRequestedAt ? ` | cancelRequestedAt=${JSON.stringify(cancelRequestedAt)}` : ''} | log=${task.log?.path || ''}${preview ? ` | tail=${JSON.stringify(preview)}` : ''}`);
391
+ const zh = String(language || '').toLowerCase().startsWith('zh');
392
+ const maxTasks = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : PROMPT_TASK_LIMIT;
393
+ const visible = tasks.slice(0, maxTasks);
394
+ const lines = [zh ? '## 可能相关的任务' : '## Possibly Relevant Tasks'];
395
+ lines.push(zh
396
+ ? '以下任务仍在后台运行。需要进度或完整输出时使用任务工具查询;不要把它们当成记忆事实。'
397
+ : 'These tasks are still running in the background. Use the task tools for progress or full output; do not treat them as memory facts.');
398
+ for (const task of visible) {
399
+ const title = promptTaskLabel(task, language);
400
+ const detail = zh
401
+ ? `${taskKindLabel(task.kind, language)},${taskStatusLabel(task.status, language)}`
402
+ : `${taskKindLabel(task.kind, language)}, ${taskStatusLabel(task.status, language)}`;
403
+ lines.push(`- ${title} (${detail})`);
404
+ }
405
+ if (tasks.length > visible.length) {
406
+ const remaining = tasks.length - visible.length;
407
+ lines.push(zh ? `- 另有 ${remaining} 个运行中任务,可用任务列表查看。` : `- ${remaining} more running task${remaining === 1 ? '' : 's'}; use the task list to inspect them.`);
370
408
  }
371
- lines.push('</active_tasks>');
372
409
  return lines.join('\n');
373
410
  }
374
411
  }
@@ -5277,12 +5277,11 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5277
5277
  const projectSessionIds = projectContext?.sessionIds || [];
5278
5278
  queryOpts.projectSessionIds = projectSessionIds;
5279
5279
  queryOpts.projectInstruction = projectContext?.projectInstruction || '';
5280
- const projectSummaries = await sharedProjectContext(ctx.CONFIG?.yeaftDir, sessionId, {
5281
- sessionIds: projectSessionIds,
5282
- language: session?.config?.language,
5283
- tokenBudget: Math.max(512, Math.floor((session?.config?.messageTokenBudget || 32768) / 8)),
5284
- });
5285
- const sharedBlock = buildProjectSharedBlock(projectContext, projectSummaries);
5280
+ // Related Session summaries now enter through Engine's single AMS
5281
+ // memory outlet. Keep this announcement limited to Project identity and
5282
+ // sharing boundaries so parent VP prompts do not duplicate the same prose
5283
+ // that sub-agents receive through memory.
5284
+ const sharedBlock = buildProjectSharedBlock(projectContext);
5286
5285
  if (sharedBlock) {
5287
5286
  queryOpts.sessionAnnouncement = queryOpts.sessionAnnouncement
5288
5287
  ? `${queryOpts.sessionAnnouncement}\n\n${sharedBlock}`