@yeaft/webchat-agent 1.0.299 → 1.0.300

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.299",
3
+ "version": "1.0.300",
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",
@@ -152,7 +152,7 @@ function sessionConversationDirs(dir) {
152
152
  * @param {string} dir — Yeaft root directory (e.g. ~/.yeaft)
153
153
  * @param {string} keyword — search terms
154
154
  * @param {number} [limit=10] — max results
155
- * @param {{telemetry?: {scannedFiles?: number, scannedBytes?: number, scannedMessages?: number}}} [options]
155
+ * @param {{telemetry?: {scannedFiles?: number, scannedBytes?: number, scannedMessages?: number}, sessionIds?: string[]}} [options]
156
156
  * @returns {object[]} — matching messages, newest first
157
157
  */
158
158
  export function searchMessages(dir, keyword, limit = 10, options = {}) {
@@ -167,18 +167,24 @@ export function searchMessages(dir, keyword, limit = 10, options = {}) {
167
167
  telemetry.scannedMessages = 0;
168
168
  }
169
169
 
170
- const conversationDirs = [
171
- { dir: join(dir, 'chat'), sessionId: null, kind: 'chat' },
172
- ...sessionConversationDirs(dir),
173
- ];
170
+ const requestedSessionIds = Array.isArray(options.sessionIds)
171
+ ? new Set(options.sessionIds.filter(id => typeof id === 'string' && id.trim()).map(id => id.trim()))
172
+ : null;
173
+ const scopedSessionDirs = sessionConversationDirs(dir)
174
+ .filter(source => !requestedSessionIds || requestedSessionIds.has(source.sessionId));
175
+ const conversationDirs = requestedSessionIds
176
+ ? scopedSessionDirs
177
+ : [{ dir: join(dir, 'chat'), sessionId: null, kind: 'chat' }, ...scopedSessionDirs];
174
178
 
175
179
  const markdownDirs = [
176
180
  ...conversationDirs.flatMap(source => [
177
181
  { ...source, dir: join(source.dir, 'messages') },
178
182
  { ...source, dir: join(source.dir, 'cold') },
179
183
  ]),
180
- { dir: join(dir, 'conversation', 'messages'), sessionId: null, kind: 'legacy-conversation' },
181
- { dir: join(dir, 'conversation', 'cold'), sessionId: null, kind: 'legacy-conversation' },
184
+ ...(!requestedSessionIds ? [
185
+ { dir: join(dir, 'conversation', 'messages'), sessionId: null, kind: 'legacy-conversation' },
186
+ { dir: join(dir, 'conversation', 'cold'), sessionId: null, kind: 'legacy-conversation' },
187
+ ] : []),
182
188
  ];
183
189
  const segmentDirs = conversationDirs.map(source => ({ ...source, dir: join(source.dir, 'segments') }));
184
190
 
package/yeaft/engine.js CHANGED
@@ -1279,6 +1279,9 @@ export class Engine {
1279
1279
  config: this.#config,
1280
1280
  taskManager: this.#taskManager,
1281
1281
  sessionId: vpCtx?.sessionId || this.#sessionId || null,
1282
+ projectSessionIds: Array.isArray(vpCtx?.projectSessionIds)
1283
+ ? vpCtx.projectSessionIds.slice()
1284
+ : [],
1282
1285
  threadId: vpCtx?.threadId || this.#currentThreadId || MAIN_THREAD_ID,
1283
1286
  currentVpId: vpCtx?.senderVpId || this.#vpId || null,
1284
1287
  // task-704b: per-tool-result hard cap derives from this. Threaded
@@ -1342,6 +1345,9 @@ export class Engine {
1342
1345
  parentVpId: vpCtx?.senderVpId || null,
1343
1346
  parentVpPersona: vpCtx?.vpPersona || null,
1344
1347
  parentSessionId: vpCtx?.sessionId || null,
1348
+ projectSessionIds: Array.isArray(vpCtx?.projectSessionIds)
1349
+ ? vpCtx.projectSessionIds.slice()
1350
+ : [],
1345
1351
  parentThreadId: vpCtx?.threadId || this.#currentThreadId || MAIN_THREAD_ID,
1346
1352
  onEvent: this.#subAgentEventSink || null,
1347
1353
  language: this.#config?.language || 'en',
@@ -1838,7 +1844,7 @@ export class Engine {
1838
1844
  * string-prompt shape (no regression for existing callers).
1839
1845
  * @yields {EngineEvent}
1840
1846
  */
1841
- async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null } = {}) {
1847
+ async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, projectSessionIds = null, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null } = {}) {
1842
1848
  if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
1843
1849
  yield {
1844
1850
  type: 'error',
@@ -1915,7 +1921,7 @@ export class Engine {
1915
1921
  };
1916
1922
  try {
1917
1923
  this.#currentThreadId = threadId || MAIN_THREAD_ID;
1918
- yield* this.#runQuery({ prompt: effectivePrompt, promptParts: effectivePromptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, askUser, threadId: this.#currentThreadId, vpTurnId, drainPendingUserMessages, prepareProviderRequest, startProviderRequest, finishProviderRequest, failProviderRequest, closePendingUserInput, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName, retryLifecycle });
1924
+ yield* this.#runQuery({ prompt: effectivePrompt, promptParts: effectivePromptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, projectSessionIds, sessionTopics, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, askUser, threadId: this.#currentThreadId, vpTurnId, drainPendingUserMessages, prepareProviderRequest, startProviderRequest, finishProviderRequest, failProviderRequest, closePendingUserInput, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName, retryLifecycle });
1919
1925
  } finally {
1920
1926
  // Closing the async generator at a visible retry boundary means the
1921
1927
  // continuation never reached a provider. Keep it out of history and
@@ -1963,7 +1969,7 @@ export class Engine {
1963
1969
  * in a try/finally without indenting the whole loop.
1964
1970
  * @private
1965
1971
  */
1966
- async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null, explicitSkillName = null, retryLifecycle }) {
1972
+ async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, projectSessionIds = null, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null, explicitSkillName = null, retryLifecycle }) {
1967
1973
 
1968
1974
  const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
1969
1975
  ? collabToolPolicy
@@ -2022,12 +2028,22 @@ export class Engine {
2022
2028
  let recallEntryCount = 0;
2023
2029
 
2024
2030
  const topicScopesForMemory = await this.#loadSessionTopicScopes(sessionId);
2031
+ const projectScopesForMemory = Array.isArray(projectSessionIds)
2032
+ ? projectSessionIds.flatMap(id => [
2033
+ `sessions/${id}`,
2034
+ `sessions/${id}/user`,
2035
+ `session/${id}`,
2036
+ `session/${id}/user`,
2037
+ `group/${id}`,
2038
+ `group/${id}/user`,
2039
+ ])
2040
+ : [];
2025
2041
  const recallResult = await this.#recallMemory(prompt, {
2026
2042
  sessionId,
2027
2043
  vpId: vpPersona && typeof vpPersona === 'object' && typeof vpPersona.vpId === 'string'
2028
2044
  ? vpPersona.vpId
2029
2045
  : (typeof senderVpId === 'string' ? senderVpId : undefined),
2030
- extraScopes: topicScopesForMemory,
2046
+ extraScopes: [...topicScopesForMemory, ...projectScopesForMemory],
2031
2047
  });
2032
2048
  recallEntryCount = recallResult && Array.isArray(recallResult.entries)
2033
2049
  ? recallResult.entries.length
@@ -3521,6 +3537,7 @@ export class Engine {
3521
3537
  router,
3522
3538
  senderVpId,
3523
3539
  sessionId: runtimeSessionId,
3540
+ projectSessionIds,
3524
3541
  threadId: runtimeThreadId,
3525
3542
  inboundEnvelope,
3526
3543
  taskId,
@@ -123,6 +123,7 @@ export function isRestrictedToolName(name) {
123
123
  * parentName?: string,
124
124
  * parentVpId?: string,
125
125
  * parentSessionId?: string|null,
126
+ * projectSessionIds?: string[],
126
127
  * parentThreadId?: string|null,
127
128
  * parentVpPersona?: object,
128
129
  * toolStats?: object,
@@ -378,6 +379,10 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
378
379
  signal: agent.abortController?.signal,
379
380
  scenario: 'chat',
380
381
  vpPersona,
382
+ sessionId: agent.parentSessionId || deps.parentSessionId || null,
383
+ projectSessionIds: Array.isArray(deps.projectSessionIds)
384
+ ? deps.projectSessionIds
385
+ : null,
381
386
  });
382
387
  for await (const evt of stream) {
383
388
  // Liveness — update first so even listener throws don't lose
@@ -138,12 +138,12 @@ export default defineTool({
138
138
  en: `Search through past conversation history.
139
139
 
140
140
  Searches message content for all whitespace-separated terms (case-insensitive).
141
- Tool-result messages are excluded. Useful for finding previous discussions, decisions, or code snippets.
141
+ Inside a Session, search is limited to that Session plus sibling Sessions in the same Project on this Agent. Tool-result messages are excluded. Useful for finding previous discussions, decisions, or code snippets.
142
142
 
143
143
  Results are returned newest-first with a bounded matching snippet and source metadata.`,
144
144
  zh: `搜索历史对话记录。
145
145
 
146
- 在已持久化消息的正文中搜索全部空格分隔的关键词(不区分大小写),并排除工具结果消息。用于查找之前的讨论、决策或代码片段。
146
+ 在已持久化消息的正文中搜索全部空格分隔的关键词(不区分大小写)。在 Session 内仅搜索当前 Session,以及同一 Agent 上同 Project 的兄弟 Session;排除工具结果消息。用于查找之前的讨论、决策或代码片段。
147
147
 
148
148
  结果按最新优先返回,包含有界的命中片段和来源信息。`
149
149
  },
@@ -180,7 +180,16 @@ Results are returned newest-first with a bounded matching snippet and source met
180
180
 
181
181
  try {
182
182
  const telemetry = {};
183
- const results = searchMessages(yeaftDir, keyword, limit, { telemetry });
183
+ const projectSessionIds = Array.isArray(ctx?.projectSessionIds)
184
+ ? ctx.projectSessionIds.filter(id => typeof id === 'string' && id.trim()).map(id => id.trim())
185
+ : [];
186
+ const scopedSessionIds = ctx?.sessionId
187
+ ? Array.from(new Set([ctx.sessionId, ...projectSessionIds]))
188
+ : null;
189
+ const results = searchMessages(yeaftDir, keyword, limit, {
190
+ telemetry,
191
+ ...(scopedSessionIds ? { sessionIds: scopedSessionIds } : {}),
192
+ });
184
193
  const searchTelemetry = {
185
194
  resultCount: results.length,
186
195
  scannedFiles: telemetry.scannedFiles || 0,
@@ -23,6 +23,7 @@
23
23
  * @property {object} [config] — engine config
24
24
  * @property {import('../tasks/manager.js').TaskManager} [taskManager] — Session task manager
25
25
  * @property {string} [sessionId] — current Session id
26
+ * @property {string[]} [projectSessionIds] — same-Agent sibling Session ids in the current Project
26
27
  * @property {string} [threadId] — current Session thread id
27
28
  * @property {string} [currentVpId] — R6: VP id of the caller (set in multi-VP groups)
28
29
  * @property {string} [currentGroupId] — R6: group id of the caller's RoleInstance
@@ -589,6 +589,54 @@ const vpCurrentTodos = new Map();
589
589
  * sessionHandle: object }>}
590
590
  */
591
591
  const sessionContexts = new Map();
592
+ /** Latest server-authoritative Project identity and same-Agent siblings per Session. */
593
+ const projectContextBySession = new Map();
594
+
595
+ function normalizeProjectContext(value, sessionId) {
596
+ if (!value || typeof value !== 'object' || !Array.isArray(value.sessionIds)) return null;
597
+ const currentSessionId = typeof sessionId === 'string' ? sessionId.trim() : '';
598
+ return {
599
+ projectId: typeof value.projectId === 'string' && value.projectId.trim()
600
+ ? value.projectId.trim()
601
+ : null,
602
+ projectName: typeof value.projectName === 'string' && value.projectName.trim()
603
+ ? value.projectName.trim()
604
+ : null,
605
+ sessionIds: Array.from(new Set(value.sessionIds
606
+ .filter(id => typeof id === 'string' && id.trim())
607
+ .map(id => id.trim())
608
+ .filter(id => id !== currentSessionId))),
609
+ };
610
+ }
611
+
612
+ function legacyProjectContext(yeaftDir, sessionId) {
613
+ const project = loadProjects(yeaftDir).find(row => row.sessionIds.includes(sessionId));
614
+ if (!project) return null;
615
+ return normalizeProjectContext({
616
+ projectId: project.id,
617
+ projectName: project.name,
618
+ sessionIds: project.sessionIds,
619
+ }, sessionId);
620
+ }
621
+
622
+ function buildProjectSharedBlock(projectContext, summaries = '') {
623
+ const context = normalizeProjectContext(projectContext, null);
624
+ const body = typeof summaries === 'string' ? summaries.trim() : '';
625
+ if (!context?.projectId && !body) return '';
626
+ const lines = ['[Project Shared Context]'];
627
+ if (context?.projectId) {
628
+ const label = context.projectName
629
+ ? `${context.projectName} (${context.projectId})`
630
+ : context.projectId;
631
+ lines.push(`Project: ${label}`);
632
+ lines.push('Sharing boundary: sibling Sessions in this Project on this Agent only.');
633
+ } else {
634
+ lines.push('Sharing boundary: sibling Sessions in the same Project on this Agent only.');
635
+ }
636
+ lines.push('Read-only memory summaries preserve each source Session identity.');
637
+ if (body) lines.push('', body);
638
+ return lines.join('\n');
639
+ }
592
640
 
593
641
  function vpKey(sessionId, vpId) {
594
642
  return `${sessionId}::${vpId}`;
@@ -2297,6 +2345,7 @@ export async function __testResetVpState() {
2297
2345
  asyncTaskOwners.clear();
2298
2346
  vpAborts.clear();
2299
2347
  sessionContexts.clear();
2348
+ projectContextBySession.clear();
2300
2349
  vpCurrentTodos.clear();
2301
2350
  threadClassifier = defaultClassifyThread;
2302
2351
  if (_vpUnsubscribe) {
@@ -3028,8 +3077,14 @@ const PROJECT_CONTEXT_MAX_TOKENS = 4096;
3028
3077
  const PROJECT_CONTEXT_TRUNCATION_NOTICE = '\n[Summary truncated to Project context budget]';
3029
3078
 
3030
3079
  async function sharedProjectContext(yeaftDir, sessionId, options = {}) {
3031
- const project = loadProjects(yeaftDir).find(row => row.sessionIds.includes(sessionId));
3032
- if (!project) return '';
3080
+ const requestedSiblingIds = Array.isArray(options.sessionIds)
3081
+ ? options.sessionIds.filter(id => typeof id === 'string' && id.trim()).map(id => id.trim())
3082
+ : null;
3083
+ const project = requestedSiblingIds === null
3084
+ ? loadProjects(yeaftDir).find(row => row.sessionIds.includes(sessionId))
3085
+ : null;
3086
+ const sourceSessionIds = requestedSiblingIds || project?.sessionIds || [];
3087
+ if (sourceSessionIds.length === 0) return '';
3033
3088
  const memoryRoot = join(yeaftDir, 'memory');
3034
3089
  const language = options.language || 'en';
3035
3090
  const configuredBudget = Number.isFinite(options.tokenBudget) && options.tokenBudget > 0
@@ -3037,7 +3092,7 @@ async function sharedProjectContext(yeaftDir, sessionId, options = {}) {
3037
3092
  : PROJECT_CONTEXT_MAX_TOKENS;
3038
3093
  const tokenBudget = Math.min(configuredBudget, PROJECT_CONTEXT_MAX_TOKENS);
3039
3094
  let context = '';
3040
- const siblingIds = project.sessionIds
3095
+ const siblingIds = sourceSessionIds
3041
3096
  .filter(id => id !== sessionId)
3042
3097
  .slice(0, PROJECT_CONTEXT_MAX_SIBLINGS);
3043
3098
  for (const siblingId of siblingIds) {
@@ -3341,6 +3396,7 @@ export function handleYeaftArchiveSession(msg) {
3341
3396
  try {
3342
3397
  const yeaftDir = ctx.CONFIG?.yeaftDir;
3343
3398
  const result = archiveSession(yeaftDir, sessionId);
3399
+ projectContextBySession.delete(sessionId);
3344
3400
  invalidateGroupContext(sessionId);
3345
3401
  sendSessionCrudResult({
3346
3402
  op: 'archive',
@@ -3362,6 +3418,7 @@ export function handleYeaftDeleteSession(msg) {
3362
3418
  try {
3363
3419
  const yeaftDir = ctx.CONFIG?.yeaftDir;
3364
3420
  const result = deleteSession(yeaftDir, sessionId);
3421
+ projectContextBySession.delete(sessionId);
3365
3422
  removeSessionFromProjects(yeaftDir, sessionId);
3366
3423
  ctx.assetOutbox?.removeSession(sessionId);
3367
3424
  // Cascade: remove every persisted message stamped with this group id.
@@ -4495,6 +4552,20 @@ async function runYeaftSessionSend(msg) {
4495
4552
  },
4496
4553
  });
4497
4554
 
4555
+ const hasInboundProjectContext = Object.prototype.hasOwnProperty.call(msg, 'projectContext');
4556
+ const inboundProjectContext = normalizeProjectContext(msg.projectContext, sessionId);
4557
+ if (hasInboundProjectContext) {
4558
+ projectContextBySession.set(sessionId, inboundProjectContext || {
4559
+ projectId: null,
4560
+ projectName: null,
4561
+ sessionIds: [],
4562
+ });
4563
+ } else {
4564
+ // An old Server does not know this field. Drop any context cached from a
4565
+ // newer Server before falling back to this Agent's legacy projects.json.
4566
+ projectContextBySession.delete(sessionId);
4567
+ }
4568
+
4498
4569
  // Ingest user text. The coordinator persists, applies mention/fanout
4499
4570
  // rules, and calls deliver() (== enqueueForVp) for each chosen VP —
4500
4571
  // which both (a) emits vp_typing_start and (b) ensures a driver runs.
@@ -4519,6 +4590,9 @@ async function runYeaftSessionSend(msg) {
4519
4590
  _promptParts: attachmentBundle.promptParts,
4520
4591
  _promptSuffix: attachmentBundle.promptSuffix,
4521
4592
  _perfTraceId: perfTraceId,
4593
+ _projectContext: msg.projectContext && typeof msg.projectContext === 'object'
4594
+ ? msg.projectContext
4595
+ : null,
4522
4596
  });
4523
4597
  } catch (err) {
4524
4598
  console.warn('[Yeaft] yeaft_session_chat: coord.ingest failed', err?.message || err);
@@ -5159,12 +5233,19 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5159
5233
  threadId,
5160
5234
  });
5161
5235
  if (queryOpts) {
5162
- const projectContext = await sharedProjectContext(ctx.CONFIG?.yeaftDir, sessionId, {
5236
+ const envelopeProjectContext = normalizeProjectContext(inboundEnvelope?._projectContext, sessionId);
5237
+ const projectContext = envelopeProjectContext
5238
+ || projectContextBySession.get(sessionId)
5239
+ || legacyProjectContext(ctx.CONFIG?.yeaftDir, sessionId);
5240
+ const projectSessionIds = projectContext?.sessionIds || [];
5241
+ queryOpts.projectSessionIds = projectSessionIds;
5242
+ const projectSummaries = await sharedProjectContext(ctx.CONFIG?.yeaftDir, sessionId, {
5243
+ sessionIds: projectSessionIds,
5163
5244
  language: session?.config?.language,
5164
5245
  tokenBudget: Math.max(512, Math.floor((session?.config?.messageTokenBudget || 32768) / 8)),
5165
5246
  });
5166
- if (projectContext) {
5167
- const sharedBlock = `[Project Shared Context]\nRead-only memory summaries from sibling Sessions in the same Project. Preserve each source Session identity.\n\n${projectContext}`;
5247
+ const sharedBlock = buildProjectSharedBlock(projectContext, projectSummaries);
5248
+ if (sharedBlock) {
5168
5249
  queryOpts.sessionAnnouncement = queryOpts.sessionAnnouncement
5169
5250
  ? `${queryOpts.sessionAnnouncement}\n\n${sharedBlock}`
5170
5251
  : sharedBlock;
@@ -7007,6 +7088,7 @@ export async function resetYeaftSession() {
7007
7088
  vpEngineConfigKeys.clear();
7008
7089
  asyncTaskOwners.clear();
7009
7090
  sessionContexts.clear();
7091
+ projectContextBySession.clear();
7010
7092
  vpCurrentTodos.clear();
7011
7093
  threadClassifier = defaultClassifyThread;
7012
7094
  // History-dedup cache is keyed by per-session coordinator msg ids;
@@ -7289,6 +7371,8 @@ export async function handleYeaftMcpReload(msg = {}) {
7289
7371
  export const __testHooks = {
7290
7372
  loadProjects,
7291
7373
  sharedProjectContext,
7374
+ buildProjectSharedBlock,
7375
+ normalizeProjectContext,
7292
7376
  loadVisibleGroupHistoryPage,
7293
7377
  projectVisibleHistoryChunkMessages,
7294
7378
  persistInboundMessageOnceByMsgId,