@yeaft/webchat-agent 0.1.597 → 0.1.599

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": "0.1.597",
3
+ "version": "0.1.599",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/unify/engine.js CHANGED
@@ -18,16 +18,19 @@
18
18
  */
19
19
 
20
20
  import { randomUUID } from 'crypto';
21
- import { buildSystemPrompt } from './prompts.js';
21
+ import { buildSystemPrompt, buildWorkerPrompt } from './prompts.js';
22
22
  import { LLMContextError, LLMAbortError } from './llm/adapter.js';
23
23
  import { recallR6, formatForInjection } from './memory/recall-r6.js';
24
24
  import { shouldConsolidate, consolidate } from './memory/consolidate.js';
25
25
  import { buildMemoryInjection } from './memory/layout.js';
26
26
  import { buildUserProfile } from './memory/user-memory-store.js';
27
+ import { readSummary as readScopeSummary } from './memory/scope-tree.js';
27
28
  import { runStopHooks } from './stop-hooks.js';
28
29
  import { getThreadStore, MAIN_THREAD_ID } from './threads/store.js';
29
30
  import { pickEffort, parseEffortPrefix } from './effort.js';
30
31
  import { normalizeEffort } from './models.js';
32
+ import { attachRouterPlan, extractPriorPlan, stripMetaForWire } from './router/continuity.js';
33
+ import { resolveThinking } from './router/thinking.js';
31
34
 
32
35
  /**
33
36
  * task-324 — Turn cap removed.
@@ -288,16 +291,56 @@ export class Engine {
288
291
  }
289
292
 
290
293
  /**
291
- * Build the system prompt with memory, compact summary, and skill content.
294
+ * Load Layer A scope summaries from `<memoryRoot>/<scope>/summary.md`.
295
+ *
296
+ * Scopes:
297
+ * - user → `user/summary.md` (always attempted)
298
+ * - group <gid> → `groups/<gid>/summary.md` (if groupId)
299
+ * - vp <vpId> → `vp/<vpId>/summary.md` (if vpId)
300
+ *
301
+ * Each fetch is best-effort — missing files / read errors return ''. The
302
+ * dream tick (Phase 6) is what populates these; on a fresh install they
303
+ * all return ''.
304
+ *
305
+ * @param {{groupId?: string, vpId?: string}} ctx
306
+ * @returns {Promise<{user:string, group:string, vp:string}>}
307
+ */
308
+ async #loadLayerASummaries({ groupId, vpId } = {}) {
309
+ if (!this.#yeaftDir) return { user: '', group: '', vp: '' };
310
+ const memoryRoot = `${this.#yeaftDir}/memory`;
311
+ const tasks = [
312
+ readScopeSummary({ kind: 'user' }, { root: memoryRoot }).catch(() => ''),
313
+ groupId
314
+ ? readScopeSummary({ kind: 'group', id: groupId }, { root: memoryRoot }).catch(() => '')
315
+ : Promise.resolve(''),
316
+ vpId
317
+ ? readScopeSummary({ kind: 'vp', id: vpId }, { root: memoryRoot }).catch(() => '')
318
+ : Promise.resolve(''),
319
+ ];
320
+ const [user, group, vp] = await Promise.all(tasks);
321
+ return { user: user || '', group: group || '', vp: vp || '' };
322
+ }
323
+
324
+ /**
325
+ * Build the system prompt with memory, compact summary, skill content,
326
+ * and (Phase 8 wire-up) Layer-A scope summaries.
327
+ *
328
+ * Routes through `buildWorkerPrompt`, which:
329
+ * - Lays in the persona-as-identity block (or Yeaft identity fallback)
330
+ * - Concatenates Layer A summaries (`user/group/vp/summary.md`)
331
+ * - Reserves Layer B / C / D placeholders for future wiring (router
332
+ * preselected memory, task scope, turn scope)
292
333
  *
293
334
  * @param {{ profile?: string, entries?: object[] }} [memory]
294
335
  * @param {string} [compactSummary]
295
336
  * @param {string} [prompt] — user prompt (for skill relevance matching)
296
- * @param {string} [memoryInjection] — task-287: prebuilt memory block (index + prefs + project)
337
+ * @param {string} [memoryInjection] — task-287: prebuilt memory block
297
338
  * @param {string} [userProfile] — user profile from user-memory shard store
339
+ * @param {object} [vpPersona]
340
+ * @param {{user?:string, group?:string, vp?:string}} [summaries]
298
341
  * @returns {string}
299
342
  */
300
- #buildSystemPrompt(memory, compactSummary, prompt, memoryInjection, userProfile, vpPersona) {
343
+ #buildSystemPrompt(memory, compactSummary, prompt, memoryInjection, userProfile, vpPersona, summaries) {
301
344
  // Get relevant skill content if SkillManager is wired
302
345
  let skillContent = '';
303
346
  if (this.#skillManager && prompt) {
@@ -309,7 +352,7 @@ export class Engine {
309
352
  ? this.#toolRegistry.getToolNames()
310
353
  : Array.from(this.#tools.keys());
311
354
 
312
- return buildSystemPrompt({
355
+ return buildWorkerPrompt({
313
356
  language: this.#config.language || 'en',
314
357
  toolNames,
315
358
  memory,
@@ -318,6 +361,11 @@ export class Engine {
318
361
  skillContent,
319
362
  userProfile,
320
363
  vpPersona,
364
+ summaries,
365
+ // Worker-shape harness is descriptive metadata for human inspection;
366
+ // production prompts skip it to save tokens. Re-enable via env when
367
+ // diagnosing prompt structure issues.
368
+ includeShape: process.env.UNIFY_PROMPT_INCLUDE_SHAPE === '1',
321
369
  // task-334f: memory_trace tool is now registered (49 → 51 tools), so
322
370
  // unlock the core_memory meta-line behind 334e's feature flag.
323
371
  memoryTraceAvailable: true,
@@ -518,7 +566,7 @@ export class Engine {
518
566
  * SCENARIO_EFFORT. Unknown values fall through to 'high'.
519
567
  * @yields {EngineEvent}
520
568
  */
521
- async *query({ prompt, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers } = {}) {
569
+ async *query({ prompt, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId } = {}) {
522
570
  if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
523
571
  yield {
524
572
  type: 'error',
@@ -569,7 +617,7 @@ export class Engine {
569
617
  const runSignal = abortCtrl.signal;
570
618
 
571
619
  try {
572
- yield* this.#runQuery({ prompt: effectivePrompt, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers });
620
+ yield* this.#runQuery({ prompt: effectivePrompt, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId });
573
621
  } finally {
574
622
  if (signal) {
575
623
  try { signal.removeEventListener('abort', onExternalAbort); } catch { /* ignore */ }
@@ -587,7 +635,7 @@ export class Engine {
587
635
  * in a try/finally without indenting the whole loop.
588
636
  * @private
589
637
  */
590
- async *#runQuery({ prompt, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers }) {
638
+ async *#runQuery({ prompt, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId }) {
591
639
 
592
640
  // ─── Pre-query: Memory Injection (task-287) + Compact Summary ──
593
641
  // Two-layer recall:
@@ -626,7 +674,21 @@ export class Engine {
626
674
 
627
675
  const compactSummary = this.#getCompactSummary();
628
676
  const userProfile = recallResult?.profile || '';
629
- const systemPrompt = this.#buildSystemPrompt(undefined, compactSummary, prompt, memoryInjection, userProfile, vpPersona);
677
+
678
+ // Phase 8 wire-up — Layer A scope summaries
679
+ // Load `summary.md` for the user / addressed group / addressed VP from
680
+ // the scoped memory tree (DESIGN.md §2). This is the rolling synopsis a
681
+ // dream tick maintains; we surface it to the worker prompt so the LLM
682
+ // has cheap, persistent context without paying the recall cost on every
683
+ // turn. Failures are non-fatal (cold-start / no memory dir).
684
+ const summaries = await this.#loadLayerASummaries({
685
+ groupId,
686
+ vpId: vpPersona && typeof vpPersona === 'object' && typeof vpPersona.vpId === 'string'
687
+ ? vpPersona.vpId
688
+ : (typeof senderVpId === 'string' ? senderVpId : undefined),
689
+ });
690
+
691
+ const systemPrompt = this.#buildSystemPrompt(undefined, compactSummary, prompt, memoryInjection, userProfile, vpPersona, summaries);
630
692
 
631
693
  // Build conversation: existing messages + new user message
632
694
  const conversationMessages = [
@@ -682,13 +744,40 @@ export class Engine {
682
744
  try {
683
745
  // task-327b: resolve effort per-turn so the long-loop auto-bump
684
746
  // kicks in once toolLoopTurns crosses the threshold.
685
- const resolvedEffort = pickEffort({ scenario, toolLoopTurns, userEffort });
747
+ let resolvedEffort = pickEffort({ scenario, toolLoopTurns, userEffort });
748
+
749
+ // DESIGN.md §9.16: thinking-mode precedence chain. When a VP
750
+ // persona is active, the router/continuity bookkeeping has more
751
+ // signal than the raw scenario tag — the prior assistant turn's
752
+ // routerPlan, the VP's role default, and the global config all
753
+ // outrank the scenario picker for `'high'|'max'`. UI/userEffort
754
+ // is already honoured by pickEffort (highest precedence).
755
+ if (vpPersona && vpPersona.vpId) {
756
+ const priorPlan = extractPriorPlan(conversationMessages, vpPersona.vpId);
757
+ const thinkingCfg = (this.#config && this.#config.thinking) || {};
758
+ const resolved = resolveThinking({
759
+ uiOverride: (userEffort === 'max' || userEffort === 'high') ? userEffort : null,
760
+ routerPlan: null, // PR-C scope: priorPlan continuity only;
761
+ // live router-plan thinking is a follow-up.
762
+ priorPlan: priorPlan && priorPlan.thinking ? priorPlan.thinking : null,
763
+ vpDefault: typeof vpPersona.thinking === 'string' ? vpPersona.thinking : null,
764
+ globalDefault: typeof thinkingCfg.default === 'string' ? thinkingCfg.default : null,
765
+ allowRouterEscalate: thinkingCfg.allowRouterEscalate !== false,
766
+ });
767
+ // Only adopt the chain's choice when it strengthens the
768
+ // baseline. We never weaken below pickEffort (e.g. consolidate
769
+ // = 'max' must not be downgraded to 'high' just because the VP
770
+ // default is 'high').
771
+ if (resolved.value === 'max' || (resolved.value === 'high' && resolvedEffort === 'low')) {
772
+ resolvedEffort = resolved.value;
773
+ }
774
+ }
686
775
 
687
776
  // Stream from adapter
688
777
  for await (const event of this.#adapter.stream({
689
778
  model: currentModel,
690
779
  system: systemPrompt,
691
- messages: [...conversationMessages],
780
+ messages: stripMetaForWire([...conversationMessages]),
692
781
  tools: toolDefs.length > 0 ? toolDefs : undefined,
693
782
  maxTokens: this.#config.maxOutputTokens || 16384,
694
783
  effort: resolvedEffort,
@@ -837,6 +926,18 @@ export class Engine {
837
926
  input: tc.input,
838
927
  }));
839
928
  }
929
+ // Phase 8 (DESIGN.md §9.15): carry the router plan back on the
930
+ // assistant message that produced it. Stripped at the wire by
931
+ // stripMetaForWire — pure bookkeeping for priorPlan continuity.
932
+ if (vpPersona && vpPersona.vpId) {
933
+ attachRouterPlan(assistantMsg, {
934
+ vpId: vpPersona.vpId,
935
+ forwardQuery: { userOriginal: prompt || '', intent: '' },
936
+ preselect: undefined,
937
+ thinking: null,
938
+ thinkingReason: '',
939
+ });
940
+ }
840
941
  conversationMessages.push(assistantMsg);
841
942
  fullResponseText += responseText;
842
943
 
package/unify/prompts.js CHANGED
@@ -155,6 +155,14 @@ function extractLangSection(content, language) {
155
155
  /** Loaded templates — read once at module load time. */
156
156
  const RAW_TEMPLATES = {
157
157
  base: readTemplate('base.md'),
158
+ // Phase 8 wire-up: split-out fragments for the persona-as-identity path.
159
+ // `identityYeaft` ships only when NO VP persona is active. `commonRules`
160
+ // ships every turn (with persona OR with Yeaft identity) — it carries
161
+ // output-format, code-editing, search, and frontend rules that are
162
+ // identity-independent. base.md remains as a back-compat bundle so any
163
+ // external snapshotter / test that reads the file directly keeps working.
164
+ identityYeaft: readTemplate('identity-yeaft.md', { required: false }),
165
+ commonRules: readTemplate('common-rules.md', { required: false }),
158
166
  modeUnified: readTemplate('mode-unified.md'),
159
167
  modeDream: readTemplate('mode-dream.md'),
160
168
  toolGuidance: readTemplate('tool-guidance.md'),
@@ -201,7 +209,7 @@ const PROMPTS = {
201
209
  coreMemoryMeta: 'To open the original message behind any entry above, call `memory_trace`.',
202
210
  vpPersonaHeader: '## active_persona',
203
211
  vpPersonaIntro: (name, role) =>
204
- `For this turn you are speaking as **${name}**${role ? ` (${role})` : ''}. Stay in character; the persona below overrides the generic Yeaft identity for tone, expertise, and decision style.`,
212
+ `You ARE **${name}**${role ? ` (${role})` : ''}. Speak in the first person as ${name}; do not refer to yourself as "Yeaft" or as a generic AI assistant. The text below is your identity, expertise, and decision style.`,
205
213
  },
206
214
  zh: {
207
215
  identity: '你是 Yeaft,一个有用的 AI 助手。',
@@ -222,7 +230,7 @@ const PROMPTS = {
222
230
  coreMemoryMeta: '如需原始 message,调 `memory_trace`。',
223
231
  vpPersonaHeader: '## active_persona',
224
232
  vpPersonaIntro: (name, role) =>
225
- `本轮你以 **${name}**${role ? `(${role})` : ''} 的身份说话。请保持人设:以下 persona 在语气、专业方向与判断风格上覆盖默认的 Yeaft 身份。`,
233
+ `你就是 **${name}**${role ? `(${role})` : ''}。请以 ${name} 的第一人称发言;不要自称 "Yeaft" 或泛指的 AI 助手。下面的文字是你的身份、专业方向与判断风格。`,
226
234
  },
227
235
  };
228
236
 
@@ -311,25 +319,28 @@ export function buildSystemPrompt({
311
319
  const parts = [];
312
320
 
313
321
  // ─── 1. Core Identity ──────────────────────────────────
314
- // Use template if available, otherwise fallback to hardcoded one-liner
315
- const baseTemplate = getTemplate('base', effectiveLang);
316
- if (baseTemplate) {
317
- parts.push(baseTemplate);
322
+ // Phase 8 wire-up: when a VP persona is active, the persona body REPLACES
323
+ // the Yeaft identity block (the LLM is that VP, not Yeaft pretending). When
324
+ // there is no persona, fall back to the legacy Yeaft identity bundle.
325
+ const personaBlock = renderVpPersona(vpPersona, lang);
326
+ if (personaBlock) {
327
+ parts.push(personaBlock);
328
+ // Common rules (output format, code editing, search, frontend) still
329
+ // apply to every turn, regardless of which VP is speaking.
330
+ const commonRules = getTemplate('commonRules', effectiveLang);
331
+ if (commonRules) parts.push(commonRules);
318
332
  } else {
319
- parts.push(lang.identity);
333
+ const baseTemplate = getTemplate('base', effectiveLang);
334
+ if (baseTemplate) {
335
+ parts.push(baseTemplate);
336
+ } else {
337
+ parts.push(lang.identity);
338
+ }
320
339
  }
321
340
 
322
341
  // ─── 2. Date Metadata ──────────────────────────────────
323
342
  parts.push(lang.date(new Date().toISOString().split('T')[0]));
324
343
 
325
- // ─── 2.5 VP Persona Override (Bug 3 fix) ───────────────
326
- // When the caller (web-bridge / dispatcher) addressed a specific VP via
327
- // @-mention, inject that VP's persona body so the LLM stops speaking as
328
- // generic Yeaft and adopts the VP's voice. Placed AFTER base identity so
329
- // the persona section's directive ("override generic Yeaft") wins.
330
- const vpBlock = renderVpPersona(vpPersona, lang);
331
- if (vpBlock) parts.push(vpBlock);
332
-
333
344
  // ─── 3. Mode-Specific Instructions ─────────────────────
334
345
  // task-297: single unified mode for all normal operation.
335
346
  // `dream` is retained for background memory maintenance.
@@ -426,10 +437,14 @@ function renderVpPersona(vpPersona, lang) {
426
437
  if (!name) return '';
427
438
  const role = typeof vpPersona.role === 'string' ? vpPersona.role.trim() : '';
428
439
  const body = typeof vpPersona.persona === 'string' ? vpPersona.persona.trim() : '';
429
- const lines = [
430
- lang.vpPersonaHeader,
431
- lang.vpPersonaIntro(name, role),
432
- ];
440
+
441
+ // Phase 8 wire-up: persona is now the IDENTITY layer (not an overlay).
442
+ // Emit a `# <name> — <role>` H1 so the prompt opens with the VP's name,
443
+ // matching the legacy Yeaft identity shape but speaking as the VP. The
444
+ // first-person imperative ("you ARE X") replaces the old soft overlay
445
+ // language so the LLM does not slip back into Yeaft voice mid-turn.
446
+ const heading = role ? `# ${name} — ${role}` : `# ${name}`;
447
+ const lines = [heading, '', lang.vpPersonaIntro(name, role)];
433
448
  if (body) lines.push('', body);
434
449
  return lines.join('\n');
435
450
  }
@@ -0,0 +1,87 @@
1
+ <!-- lang:en -->
2
+
3
+ ## Core Principles
4
+
5
+ - You are a thoughtful collaborator, not just a command executor
6
+ - Admit uncertainty honestly — say "I'm not sure" rather than guessing
7
+ - Cite evidence when making claims about code, behavior, or facts
8
+ - Be concise: prefer short, direct answers over verbose explanations
9
+ - Never add emoji unless the user uses them first
10
+ - Never start responses with excessive flattery ("Great question!")
11
+
12
+ ## Output Format
13
+
14
+ - Use GitHub-flavored Markdown
15
+ - Code blocks must include language identifiers: ```js, ```python, etc.
16
+ - Reference files with inline code: `src/app.ts:42`
17
+ - Avoid deeply nested bullet lists — prefer flat structure or numbered steps
18
+ - For terminal commands, use single-line code blocks
19
+ - For multi-step instructions, use numbered lists
20
+
21
+ ## Code Editing Rules
22
+
23
+ - Always read a file before editing it
24
+ - Never revert changes you did not make
25
+ - Never amend commits unless the user explicitly asks
26
+ - Never use `git reset --hard` or `git clean -f` without user approval
27
+ - Prefer non-interactive git commands (no `git rebase -i`, no `git add -i`)
28
+ - Default to ASCII — avoid Unicode decorations in code
29
+ - Follow existing code style: indentation, naming conventions, patterns
30
+ - When adding code, match the surrounding context
31
+
32
+ ## Search and Navigation
33
+
34
+ - Prefer `rg` (ripgrep) over `grep` for speed and regex support
35
+ - Use `glob` patterns for file discovery
36
+ - Read files with offset/limit for large files instead of loading everything
37
+
38
+ ## Frontend Design (when applicable)
39
+
40
+ - Avoid "AI slop": no gratuitous purple gradients, no hero sections with vague taglines
41
+ - Do not default to dark theme — follow project conventions
42
+ - Match existing design system; do not introduce new component libraries without asking
43
+ - Prefer semantic HTML and progressive enhancement
44
+
45
+ <!-- lang:zh -->
46
+
47
+ ## 核心原则
48
+
49
+ - 你是一个深思熟虑的协作者,而非单纯的命令执行器
50
+ - 诚实地承认不确定性 — 说"我不确定"而不是猜测
51
+ - 在对代码、行为或事实做出断言时引用证据
52
+ - 简洁:优先使用简短直接的回答,而非冗长的解释
53
+ - 除非用户先使用 emoji,否则不要添加
54
+ - 不要以过度的奉承开头("好问题!")
55
+
56
+ ## 输出格式
57
+
58
+ - 使用 GitHub 风格的 Markdown
59
+ - 代码块必须包含语言标识:```js、```python 等
60
+ - 使用内联代码引用文件:`src/app.ts:42`
61
+ - 避免深层嵌套的项目列表 — 优先使用扁平结构或编号步骤
62
+ - 终端命令使用单行代码块
63
+ - 多步骤指令使用编号列表
64
+
65
+ ## 代码编辑规则
66
+
67
+ - 编辑文件前必须先读取
68
+ - 不要回退你未做的修改
69
+ - 除非用户明确要求,否则不要 amend commit
70
+ - 未经用户同意不使用 `git reset --hard` 或 `git clean -f`
71
+ - 优先使用非交互式 git 命令(不用 `git rebase -i`、不用 `git add -i`)
72
+ - 默认使用 ASCII — 避免在代码中使用 Unicode 装饰
73
+ - 遵循已有的代码风格:缩进、命名约定、模式
74
+ - 添加代码时匹配周围的上下文
75
+
76
+ ## 搜索与导航
77
+
78
+ - 优先使用 `rg`(ripgrep)而非 `grep`,速度更快且支持正则
79
+ - 使用 `glob` 模式发现文件
80
+ - 对大文件使用 offset/limit 读取,而非加载全部内容
81
+
82
+ ## 前端设计(适用时)
83
+
84
+ - 避免 "AI 泛滥风格":不要无端使用紫色渐变、不要带模糊标语的 hero 区域
85
+ - 不要默认使用暗色主题 — 遵循项目约定
86
+ - 匹配现有设计系统;不要在未询问的情况下引入新的组件库
87
+ - 优先使用语义化 HTML 和渐进增强
@@ -0,0 +1,11 @@
1
+ <!-- lang:en -->
2
+
3
+ # Yeaft — AI Companion
4
+
5
+ You are Yeaft, an AI companion that maintains a single continuous conversation with the user. You remember context across sessions through your memory system. Every interaction builds on what came before.
6
+
7
+ <!-- lang:zh -->
8
+
9
+ # Yeaft — AI 伙伴
10
+
11
+ 你是 Yeaft,一个与用户保持单一持续对话的 AI 伙伴。你通过记忆系统在会话间记住上下文。每次交互都建立在之前的基础上。
@@ -97,7 +97,7 @@ export class EngineInstance {
97
97
  * @param {AbortSignal} [params.signal]
98
98
  * @yields {object} EngineEvent with { ...event, threadId }
99
99
  */
100
- async *query({ prompt, mode, signal, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers } = {}) {
100
+ async *query({ prompt, mode, signal, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId } = {}) {
101
101
  if (this.#terminated) {
102
102
  yield {
103
103
  type: 'error',
@@ -173,7 +173,7 @@ export class EngineInstance {
173
173
  curToolResults = [];
174
174
  }
175
175
 
176
- for await (const event of this.#engine.query({ prompt, mode, messages: snapshot, signal, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers })) {
176
+ for await (const event of this.#engine.query({ prompt, mode, messages: snapshot, signal, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId })) {
177
177
  // Re-tag every event with the bound threadId. Non-object events
178
178
  // (shouldn't happen — all engine events are objects) are passed
179
179
  // through untouched.
@@ -1256,13 +1256,17 @@ export async function handleUnifyGroupChat(msg) {
1256
1256
  * Returns `undefined` when we have nothing to inject — keeps the legacy
1257
1257
  * single-agent path identical to before.
1258
1258
  */
1259
- function buildVpQueryOpts({ vpId, groupCoordinator }) {
1259
+ function buildVpQueryOpts({ vpId, groupCoordinator, groupId }) {
1260
1260
  if (!vpId) return undefined;
1261
1261
  const out = { senderVpId: vpId };
1262
+ if (typeof groupId === 'string' && groupId.trim()) {
1263
+ out.groupId = groupId.trim();
1264
+ }
1262
1265
  try {
1263
1266
  const vp = readVp(vpId);
1264
1267
  if (vp) {
1265
1268
  out.vpPersona = {
1269
+ vpId,
1266
1270
  displayName: vp.displayName || vpId,
1267
1271
  role: vp.role || '',
1268
1272
  persona: vp.persona || '',
@@ -1411,7 +1415,7 @@ export async function handleUnifyChat(msg) {
1411
1415
  const { entry } = session.dispatcher.submit(cleanedPrompt, {
1412
1416
  messageId: msg.messageId,
1413
1417
  override: override || undefined,
1414
- queryOpts: buildVpQueryOpts({ vpId, groupCoordinator }),
1418
+ queryOpts: buildVpQueryOpts({ vpId, groupCoordinator, groupId }),
1415
1419
  });
1416
1420
  sendUnifyEvent({
1417
1421
  type: 'input_queue_updated',