@yeaft/webchat-agent 1.0.513 → 1.0.515

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/yeaft/prompts.js CHANGED
@@ -9,12 +9,10 @@
9
9
  *
10
10
  * Concept layering (DESIGN-PROMPT §3):
11
11
  * ① Identity — VP persona body (or Yeaft fallback)
12
- * ② Rules — session announcement, date, mode template, tools,
13
- * tool-guidance, skills, common rules
12
+ * ② Rules — Session/Project instructions, date, runtime, and skills
14
13
  * ③ Memory — single block produced upstream by the AMS render
15
14
  * outlet and threaded through here as `memoryInjection`
16
- * ④ Active Scope — structured per-turn scope summary
17
- * (session / vp / members / envelope IDs)
15
+ * ④ Routing — multi-VP identity and handoff metadata when applicable
18
16
  *
19
17
  * Long-term semantic context comes only from the AMS Memory outlet. The
20
18
  * conversation transcript stays in the messages timeline and is bounded by
@@ -142,7 +140,6 @@ const RAW_TEMPLATES = {
142
140
  commonRules: readTemplate('common-rules.md', { required: false }),
143
141
  modeUnified: readTemplate('mode-unified.md'),
144
142
  modeDream: readTemplate('mode-dream.md'),
145
- toolGuidance: readTemplate('tool-guidance.md'),
146
143
  // Phase 1 — DESIGN.md "Migration Plan" harness fragments. Optional so
147
144
  // older deployments without the templates still boot; buildWorkerPrompt /
148
145
  // buildRouterPrompt callers will simply omit the section.
@@ -192,12 +189,6 @@ const PROMPTS = {
192
189
  identity: 'No VP soul is active for this turn. Participate in the current session with grounded, evidence-based answers and preserve the user\'s context.',
193
190
  date: (d) => `Date: ${d}`,
194
191
  dream: 'You are in dream mode. Reflect on past conversations and consolidate memories.',
195
- // DESIGN-PROMPT §3 ④ — current session context block.
196
- activeScopeHeader: '## Current session context',
197
- activeScopeSessionIdLabel: 'Session ID',
198
- activeScopeMembersLabel: 'Session members',
199
- activeScopeTopicsLabel: 'Current focus',
200
- activeScopeEnvelopeLabel: 'Handoff',
201
192
  multiVpRoutingHeader: '## multi_vp_routing',
202
193
  sessionAnnouncementHeader: '[Session Announcement]',
203
194
  projectInstructionHeader: '[Project Instruction]',
@@ -218,12 +209,6 @@ const PROMPTS = {
218
209
  identity: '你正在当前会话中参与协作。保持用户上下文,回答要基于证据;需要工具时使用工具,但不要把自己没有实际执行过的事说成已经执行。',
219
210
  date: (d) => `日期:${d}`,
220
211
  dream: '你处于梦境模式。回顾过去的对话,整理和巩固记忆。',
221
- // DESIGN-PROMPT §3 ④ — 当前会话上下文。
222
- activeScopeHeader: '## 当前会话上下文',
223
- activeScopeSessionIdLabel: '会话 ID',
224
- activeScopeMembersLabel: '会话成员',
225
- activeScopeTopicsLabel: '当前讨论',
226
- activeScopeEnvelopeLabel: '转交消息',
227
212
  multiVpRoutingHeader: '## multi_vp_routing',
228
213
  sessionAnnouncementHeader: '[会话公告]',
229
214
  projectInstructionHeader: '[Project 指令]',
@@ -275,28 +260,22 @@ export function normalizePromptLanguage(language) {
275
260
  *
276
261
  * Prompt structure (DESIGN-PROMPT §3):
277
262
  * ① Identity — Core identity (persona or Yeaft fallback)
278
- * ② Rules — Session announcement, date, mode, tools, guidance, skills
263
+ * ② Rules — Session/Project instructions, date, runtime, and skills
279
264
  * ③ Memory — Single block produced by the AMS render outlet
280
265
  * (callers pass it as `memoryInjection`).
281
- * ④ Active Scope — Structured per-turn scope summary
282
- * (session / vp / members / envelope IDs).
283
- * (The previous standalone user_profile / core_memory blocks are
284
- * gone — those signals now arrive through AMS Resident. Task
285
- * context (`taskCtx`) was wired into Active Scope by task-334e
286
- * but never actually populated by the engine; removed 2026-05-13.)
266
+ * ④ Routing — Multi-VP identity and handoff metadata when applicable
287
267
  *
288
- * Active Scope params (DESIGN-PROMPT §3 ④):
289
- * @param {object} [activeScope] — structured scope summary for this turn
290
- * @param {string} [activeScope.sessionId]
291
- * @param {string} [activeScope.sessionMember]
292
- * @param {string[]} [activeScope.sessionMembers] current session roster
293
- * @param {string[]} [activeScope.sessionTopics] bounded topic labels for this session
294
- * @param {object} [activeScope.envelope] inbound routing info (sender, intent)
268
+ * Generic Session IDs, inferred topics, active-task labels, and repeated tool
269
+ * guidance are intentionally omitted from the system prompt.
270
+ *
271
+ * Multi-VP routing params:
272
+ * @param {object} [activeScope] — routing scope for this turn
273
+ * @param {string} [activeScope.vpId] current VP identity
274
+ * @param {string[]} [activeScope.sessionMembers] current Session roster
295
275
  *
296
276
  * @param {{
297
277
  * language?: string,
298
278
  * mode?: string,
299
- * toolNames?: string[],
300
279
  * memoryInjection?: string,
301
280
  * skillContent?: string,
302
281
  * activeScope?: object,
@@ -313,7 +292,6 @@ export function normalizePromptLanguage(language) {
313
292
  export function buildSystemPrompt({
314
293
  language = 'en',
315
294
  mode,
316
- toolNames = [],
317
295
  memoryInjection,
318
296
  skillContent,
319
297
  activeScope,
@@ -324,7 +302,6 @@ export function buildSystemPrompt({
324
302
  workCenterInstructions = '',
325
303
  projectDoc = '',
326
304
  runtimePlatform,
327
- activeTasks = '',
328
305
  promptNotices = [],
329
306
  } = {}) {
330
307
  // Normalize app locales like `zh-CN` to prompt dictionary/template keys.
@@ -405,20 +382,15 @@ export function buildSystemPrompt({
405
382
  parts.push(dreamTemplate || lang.dream);
406
383
  }
407
384
 
408
- // ─── 4. Runtime Platform + Tools + Tool Guidance ──────
385
+ // ─── 4. Runtime Platform ─────────────────────────────
386
+ // Tool schemas already carry their operational guidance. Repeating selected
387
+ // tool instructions in the system prompt wastes tokens and can drift from
388
+ // the executable schema, so only platform facts are rendered here.
409
389
  const runtimePlatformBlock = renderRuntimePlatformPrompt(runtimePlatform || getRuntimePlatformInfo(), effectiveLang);
410
390
  if (runtimePlatformBlock) {
411
391
  parts.push(runtimePlatformBlock);
412
392
  }
413
393
 
414
- if (toolNames.length > 0) {
415
- const guidance = renderActiveToolGuidance(toolNames, effectiveLang);
416
- const toolGuidanceTemplate = getTemplate('toolGuidance', effectiveLang);
417
- if (guidance && toolGuidanceTemplate) {
418
- parts.push(toolGuidanceTemplate.replace('{{guidance}}', guidance));
419
- }
420
- }
421
-
422
394
  // ─── 5. Skills Section ─────────────────────────────────
423
395
  if (skillContent) {
424
396
  parts.push(skillContent);
@@ -436,16 +408,10 @@ export function buildSystemPrompt({
436
408
  parts.push(memoryInjection.trim());
437
409
  }
438
410
 
439
- // ─── 7. Active Scope (DESIGN-PROMPT §3 ④) ──────────────
440
- // Structured per-turn scope summary. The group/vp/envelope identifiers
441
- // are rendered as a leading line. (Per-task taskCtx sub-block was
442
- // never wired and is removed 2026-05-13.)
443
- const activeScopeBlock = renderActiveScope(activeScope, lang);
444
- if (activeScopeBlock) parts.push(activeScopeBlock);
445
-
446
- const activeTaskText = typeof activeTasks === 'string' ? activeTasks.trim() : '';
447
- if (activeTaskText) parts.push(activeTaskText);
448
-
411
+ // ─── 7. Multi-VP routing ────────────────────────────────
412
+ // Generic Session IDs, inferred topics, and live task labels are runtime
413
+ // bookkeeping rather than model instructions. Keep only the routing block
414
+ // whose peer identities and RouteForward contract affect model behaviour.
449
415
  const multiVpRoutingBlock = renderMultiVpRouting(activeScope, lang);
450
416
  if (multiVpRoutingBlock) parts.push(multiVpRoutingBlock);
451
417
 
@@ -454,64 +420,6 @@ export function buildSystemPrompt({
454
420
 
455
421
  // ─── helpers ─────────────────────────────────────────────────────
456
422
 
457
- const TOOL_GUIDANCE_GROUPS = Object.freeze([
458
- {
459
- tools: ['DiscoverTools'],
460
- en: 'If the visible tools do not clearly cover the request, use `DiscoverTools` with the user goal before concluding that a capability is unavailable. If the target is absent from a page, follow `next_cursor` until found or the hidden directory is exhausted. If `restart_required` is true, restart without a cursor because the registered directory changed.',
461
- zh: '如果可见工具不能明确覆盖请求,应先按用户目标调用 `DiscoverTools`。若当前页没有目标,应按 `next_cursor` 继续翻页,直到找到或隐藏目录耗尽后,才能判断某项能力不可用。如果 `restart_required` 为 true,说明注册目录已变化,应丢弃游标重新开始。',
462
- },
463
- {
464
- tools: ['FileRead', 'FileWrite', 'FileEdit', 'Glob', 'Grep', 'ListDir', 'ApplyPatch', 'NotebookEdit'],
465
- en: 'Read existing files before editing. Use dedicated file/search tools instead of shell search or `sed -i`; make small, reviewable edits. Parallelize only proven-independent reads under the accuracy-first rule above.',
466
- zh: '编辑前先读现有文件。文件搜索和修改优先使用专用工具,不用 shell 搜索或 `sed -i`;改动保持小而可审查。只有明确满足上述准确性优先判据的读取才可并行。',
467
- },
468
- {
469
- tools: ['Bash'],
470
- en: 'Use non-interactive, deterministic shell commands, set reasonable timeouts, quote paths with spaces, and do not run destructive operations without authorization.',
471
- zh: 'Shell 命令保持非交互、确定性并设置合理 timeout;包含空格的路径要引用,未经授权不要执行破坏性操作。',
472
- },
473
- {
474
- tools: ['TodoWrite'],
475
- en: 'For non-trivial multi-step work, write a brief visible plan and call `TodoWrite` in the same assistant response as the first necessary work-tool call only when its arguments and safety do not depend on another result. Start with the smallest such call; do not speculative-batch the investigation or stop after planning unless user input genuinely blocks the first step.',
476
- zh: '非平凡多步骤任务先写简短可见计划。只有第一个工作工具调用已经确定有必要,且其参数和安全性都不依赖其他结果时,才在同一个 assistant response 中把它与 `TodoWrite` 一起发出;先执行满足条件的最小调用,不要推测性批量展开调查。只有用户信息确实阻塞第一步时才在规划后停下。',
477
- },
478
- {
479
- tools: ['SpawnAgent', 'PromptAgent', 'WaitAgent', 'CloseAgent', 'ListAgents'],
480
- en: 'Delegate only independent, bounded work. Keep ownership in the parent. After PromptAgent queues follow-up work, call WaitAgent in the same parent turn and collect the reply before ending. If a bounded wait times out, wait again with a larger bound unless the agent is stale/stalled. Relay the reply or continue the dependent work, then close the sub-agent when it is no longer needed.',
481
- zh: '只委派边界清晰且独立的工作。父级保留任务所有权。PromptAgent 排队后续工作后,必须在同一个父级 turn 调用 WaitAgent 并拿到回复再结束;有界等待超时后,除非 Agent 已 stale/stalled,否则使用更大上限继续等待;随后转述结果或继续依赖该结果的工作,不再需要时关闭子 Agent。',
482
- },
483
- {
484
- tools: ['ListTasks', 'ReadTaskLog', 'CancelTask'],
485
- en: 'Treat background tasks as live execution state, not memory facts. Inspect status or logs before retrying or cancelling work.',
486
- zh: '后台任务是实时执行状态,不是记忆事实。重试或取消前先检查状态或日志。',
487
- },
488
- {
489
- tools: ['RouteForward'],
490
- en: 'Use `RouteForward` for explicit VP-to-VP handoff; writing an @mention in ordinary text does not dispatch another VP.',
491
- zh: '显式 VP 转交必须使用 `RouteForward`;普通文本中的 @mention 不会调度另一个 VP。',
492
- },
493
- {
494
- tools: ['CreateWorkItem'],
495
- en: 'Use `CreateWorkItem` only for goals that need durable cross-turn coordination, recovery, review, waiting, or retry.',
496
- zh: '只有目标需要跨 turn 持久协调、恢复、评审、等待或重试时才使用 `CreateWorkItem`。',
497
- },
498
- ]);
499
-
500
- function renderActiveToolGuidance(toolNames, language) {
501
- const active = new Set(Array.isArray(toolNames) ? toolNames : []);
502
- const lines = [];
503
- if (active.size > 0) {
504
- lines.push(language === 'zh'
505
- ? '- 准确性优先:先用能解决当前未知的最小定向调用。只有每个调用都已经确定有必要,且其参数和安全性都不依赖同批其他结果时,才在一个响应中发出多个工具调用;否则串行执行。不要推测性扇出、重复成功的读取/搜索,也不要默认抓取多个来源;先检查证据,再决定是否扩展。'
506
- : '- Accuracy first: start with the smallest targeted call that can resolve the current uncertainty. Issue multiple tool calls in one response only when every call is already necessary and its arguments and safety do not depend on another call\'s result. Otherwise run them sequentially. Do not fan out speculatively, repeat a successful read/search, or fetch multiple sources by default; inspect evidence before expanding.');
507
- }
508
- for (const group of TOOL_GUIDANCE_GROUPS) {
509
- if (!group.tools.some(name => active.has(name))) continue;
510
- lines.push(`- ${language === 'zh' ? group.zh : group.en}`);
511
- }
512
- return lines.join('\n');
513
- }
514
-
515
423
  /**
516
424
  * Render the VP identity block when the engine is running on behalf of an
517
425
  * addressed VP. The `persona` body from role.md is the only soul source;
@@ -570,32 +478,6 @@ function selectVpPersonaBody(vpPersona, effectiveLang) {
570
478
 
571
479
 
572
480
 
573
- function renderActiveScope(activeScope, lang) {
574
- if (!activeScope || typeof activeScope !== 'object') return '';
575
-
576
- const isZh = lang === PROMPTS.zh;
577
- const separator = isZh ? ':' : ': ';
578
- const lines = [];
579
- const session = typeof activeScope.sessionId === 'string' && activeScope.sessionId.trim()
580
- ? activeScope.sessionId.trim()
581
- : '';
582
- if (session) lines.push(`${lang.activeScopeSessionIdLabel}${separator}${session}`);
583
-
584
- const membersLine = renderSessionMembersLine(activeScope.sessionMembers || activeScope.members, isZh);
585
- if (membersLine) lines.push(`${lang.activeScopeMembersLabel}${separator}${membersLine}`);
586
-
587
- const topicsLine = renderSessionTopicsLine(activeScope.sessionTopics, isZh);
588
- if (topicsLine) lines.push(`${lang.activeScopeTopicsLabel}${separator}${topicsLine}`);
589
-
590
- const envLine = renderEnvelopeLine(activeScope.envelope, isZh);
591
- if (envLine) lines.push(`${lang.activeScopeEnvelopeLabel}${separator}${envLine}`);
592
-
593
- if (lines.length === 0) return '';
594
-
595
- return `${lang.activeScopeHeader}\n${lines.join('\n')}`;
596
- }
597
-
598
-
599
481
  function firstNonEmptyString(...values) {
600
482
  for (const value of values) {
601
483
  if (typeof value === 'string' && value.trim()) return value.trim();
@@ -603,151 +485,6 @@ function firstNonEmptyString(...values) {
603
485
  return '';
604
486
  }
605
487
 
606
- function renderSessionMembersLine(members, useChineseSeparator = false) {
607
- return normalizeSessionMemberIds(members).join(useChineseSeparator ? '、' : ', ');
608
- }
609
-
610
- function renderSessionTopicsLine(topics, isZh = false) {
611
- const descriptions = [];
612
- const seen = new Set();
613
- for (const topic of normalizeSessionTopicIds(topics)) {
614
- const description = describeSessionTopic(topic, isZh);
615
- if (!description || seen.has(description)) continue;
616
- seen.add(description);
617
- descriptions.push(description);
618
- }
619
- return descriptions.join(isZh ? ';' : '; ');
620
- }
621
-
622
- function normalizeSessionTopicIds(topics) {
623
- if (!Array.isArray(topics)) return [];
624
- const clean = [];
625
- const seen = new Set();
626
- for (const topic of topics) {
627
- if (typeof topic !== 'string') continue;
628
- const id = topic.trim();
629
- if (!id || seen.has(id)) continue;
630
- seen.add(id);
631
- clean.push(id);
632
- }
633
- return clean;
634
- }
635
-
636
- function describeSessionTopic(topic, isZh = false) {
637
- const normalized = topic.toLowerCase().replace(/[_\s]+/g, '-');
638
-
639
- if (normalized.includes('dream') && normalized.includes('segments')) {
640
- return isZh
641
- ? '梦境记忆片段的抽取与整理'
642
- : 'Dream memory segment extraction and organization';
643
- }
644
- if (normalized.includes('dream') && normalized.includes('session') && normalized.includes('extraction')) {
645
- return isZh
646
- ? '梦境会话记忆的抽取质量'
647
- : 'Dream session-memory extraction quality';
648
- }
649
- if (normalized.includes('system-prompt') && normalized.includes('localization')) {
650
- return isZh
651
- ? '系统提示词的中英文一致性'
652
- : 'system prompt language localization';
653
- }
654
- if (normalized.includes('default-character') && normalized.includes('prompt-soul')) {
655
- return isZh
656
- ? '默认角色灵魂提示词的表达方式'
657
- : 'default persona soul prompt wording';
658
- }
659
- if (normalized.includes('prompt') && normalized.includes('soul')) {
660
- return isZh
661
- ? '角色灵魂提示词的表达方式'
662
- : 'persona soul prompt wording';
663
- }
664
- if (normalized.includes('openai-responses')) {
665
- return isZh
666
- ? 'Yeaft 的 OpenAI Responses 适配与模型配置'
667
- : 'Yeaft OpenAI Responses adapter and model configuration';
668
- }
669
- if (normalized.includes('model-config-isolation')) {
670
- return isZh
671
- ? 'Yeaft 的模型配置隔离'
672
- : 'Yeaft model configuration isolation';
673
- }
674
- if (normalized.includes('route-forward') || normalized.includes('handoff')) {
675
- return isZh
676
- ? 'Yeaft 的会话路由交接与可见性'
677
- : 'Yeaft session routing handoff and visibility';
678
- }
679
- if (normalized.includes('copilot-cli') || normalized.includes('chat-session')) {
680
- return isZh
681
- ? 'Copilot CLI 的聊天会话行为'
682
- : 'Copilot CLI chat session behavior';
683
- }
684
- if (normalized.includes('claude-opus')) {
685
- return isZh
686
- ? 'Yeaft 的 Claude Opus 模型接入'
687
- : 'Yeaft Claude Opus model integration';
688
- }
689
- if (normalized.includes('pr-workflow')) {
690
- return isZh
691
- ? '项目的 PR review、merge 和 tag 发布流程'
692
- : 'project PR review, merge, and tag release workflow';
693
- }
694
- if (/\bpr[-/]?\d+\b/.test(normalized) || normalized.includes('release') || /v\d+\.\d+\.\d+/.test(normalized)) {
695
- return isZh
696
- ? '最近的 PR 修复、review、merge 和 tag 发布流程'
697
- : 'recent PR fixes, review, merge, and tag release work';
698
- }
699
- if (normalized.includes('active-scope')) {
700
- return isZh
701
- ? '当前会话上下文的提示词呈现'
702
- : 'current session context prompt rendering';
703
- }
704
- if (normalized.includes('prompt') || normalized.includes('system')) {
705
- return isZh
706
- ? '近期的系统提示词调整'
707
- : humanizeTopicSlug(topic, false) || 'recent system prompt work';
708
- }
709
- if (normalized.includes('dream')) {
710
- return isZh
711
- ? '近期的 Dream 记忆维护工作'
712
- : humanizeTopicSlug(topic, false) || 'recent Dream memory work';
713
- }
714
- if (normalized.includes('session')) {
715
- return isZh
716
- ? '近期的会话上下文调整'
717
- : humanizeTopicSlug(topic, false) || 'recent session context work';
718
- }
719
-
720
- if (isZh) return '近期的项目协作事项';
721
- return humanizeTopicSlug(topic, false) || 'recent session collaboration topics';
722
- }
723
-
724
- function humanizeTopicSlug(topic, isZh = false) {
725
- if (isZh) return '';
726
- const words = topic
727
- .replace(/[\/_-]+/g, ' ')
728
- .replace(/\bv\d+(?:\.\d+)+\b/gi, '')
729
- .replace(/\bpr\s*\d+\b/gi, '')
730
- .trim()
731
- .split(/\s+/)
732
- .filter(Boolean);
733
- if (words.length === 0) return '';
734
-
735
- const normalizedWords = words.map(word => {
736
- const lower = word.toLowerCase();
737
- if (lower === 'yeaft') return 'Yeaft';
738
- if (lower === 'project') return 'project';
739
- if (lower === 'config') return 'configuration';
740
- if (lower === 'isolation') return 'isolation';
741
- if (lower === 'rendering') return 'rendering';
742
- if (lower === 'workflow') return 'workflow';
743
- if (lower === 'session') return 'session';
744
- if (lower === 'context') return 'context';
745
- return word;
746
- });
747
-
748
- return normalizedWords.join(' ');
749
- }
750
-
751
488
  function normalizeSessionMemberIds(members) {
752
489
  if (!Array.isArray(members)) return [];
753
490
  const clean = [];
@@ -764,7 +501,7 @@ function normalizeSessionMemberIds(members) {
764
501
 
765
502
  function renderMultiVpRouting(activeScope, lang) {
766
503
  if (!activeScope || typeof activeScope !== 'object') return '';
767
- const ownId = firstNonEmptyString(activeScope.sessionMember, activeScope.vpId);
504
+ const ownId = firstNonEmptyString(activeScope.vpId);
768
505
  const members = normalizeSessionMemberIds(activeScope.sessionMembers || activeScope.members);
769
506
  const peers = ownId ? members.filter((member) => member !== ownId) : members;
770
507
  if (peers.length === 0) return '';
@@ -793,33 +530,6 @@ function renderMultiVpRouting(activeScope, lang) {
793
530
  ].join('\n');
794
531
  }
795
532
 
796
- /**
797
- * Render a one-line envelope summary. Pulls the small set of routing
798
- * fields we surface to the LLM (sender, intent, originating user) and
799
- * leaves the rest in AMS. Returns '' when the envelope carries no
800
- * useful signal.
801
- *
802
- * @param {object|null|undefined} envelope
803
- * @returns {string}
804
- */
805
- function renderEnvelopeLine(envelope, isZh = false) {
806
- if (!envelope || typeof envelope !== 'object') return '';
807
- const segments = [];
808
- const fromVp = typeof envelope.fromVpId === 'string' && envelope.fromVpId.trim()
809
- ? envelope.fromVpId.trim()
810
- : (typeof envelope.senderVpId === 'string' ? envelope.senderVpId.trim() : '');
811
- if (fromVp) segments.push(`${isZh ? '来自' : 'from'}=${fromVp}`);
812
- const fromUser = typeof envelope.fromUserId === 'string' && envelope.fromUserId.trim()
813
- ? envelope.fromUserId.trim()
814
- : '';
815
- if (fromUser) segments.push(`${isZh ? '用户' : 'user'}=${fromUser}`);
816
- const intent = typeof envelope.intent === 'string' && envelope.intent.trim()
817
- ? envelope.intent.trim()
818
- : '';
819
- if (intent) segments.push(`${isZh ? '意图' : 'intent'}=${intent}`);
820
- return segments.join(' ');
821
- }
822
-
823
533
  // ─── Phase 1: Worker / Router prompt splits ──────────────────────
824
534
  //
825
535
  // DESIGN.md (multi-VP redesign) describes two distinct prompt shapes:
@@ -830,7 +540,7 @@ function renderEnvelopeLine(envelope, isZh = false) {
830
540
  // carries Layer-A summaries + UserProfile + CoreMemory, AMS OnDemand
831
541
  // carries the per-turn FTS hits. The worker shape that survives is:
832
542
  // harness/worker-shape — optional descriptive metadata
833
- // buildSystemPrompt(...) — ① Identity ② Rules ③ Memory ④ Active Scope
543
+ // buildSystemPrompt(...) — ① Identity ② Rules ③ Memory ④ Routing
834
544
  // optional taskScope/turnScope — caller-provided pass-through strings
835
545
  // `renderLayerASummaries` is no longer called inside the worker prompt
836
546
  // because AMS already renders the same summaries — calling both was
@@ -891,15 +601,12 @@ export function renderLayerASummaries(summaries, language = 'en') {
891
601
  *
892
602
  * Output sections (DESIGN-PROMPT §3 layered concepts):
893
603
  * harness/worker-shape (optional) — descriptive metadata
894
- * buildSystemPrompt(...) — ① Identity ② Rules ③ Memory ④ Active Scope
604
+ * buildSystemPrompt(...) — ① Identity ② Rules ③ Memory ④ Routing
895
605
  *
896
- * Earlier task-322 / task-334e variants accepted `taskScope` and
897
- * `turnScope` pass-through strings so callers could append their own
898
- * scope blocks. DESIGN-PROMPT v1 retired that surface — Active Scope is
899
- * now structured (`activeScope: { sessionId, vpId, envelope }`) and
900
- * rendered by `buildSystemPrompt` itself. Both pass-through params
901
- * had zero remaining callers when v1 landed; removing them prevents the
902
- * "two ways to describe scope" drift §1 set out to eliminate.
606
+ * Earlier variants accepted `taskScope` and `turnScope` pass-through
607
+ * strings so callers could append their own scope blocks. That surface is
608
+ * retired. `activeScope` now carries only the VP identity and Session roster
609
+ * needed to render the deterministic multi-VP routing contract.
903
610
  *
904
611
  * @param {{
905
612
  * language?: 'en'|'zh',
@@ -924,7 +631,7 @@ export function buildWorkerPrompt(params = {}) {
924
631
  if (shape) parts.push(shape);
925
632
  }
926
633
 
927
- // Identity + Rules + Memory + Active Scope (DESIGN-PROMPT §3).
634
+ // Identity + Rules + Memory + Routing (DESIGN-PROMPT §3).
928
635
  const baseBlock = buildSystemPrompt({ ...rest, language: effectiveLang });
929
636
  if (baseBlock) parts.push(baseBlock);
930
637
 
@@ -42,6 +42,7 @@ export {
42
42
  makeSessionId,
43
43
  ensureDefaultSessionIfEmpty,
44
44
  createSessionFromSpec,
45
+ copySession,
45
46
  renameSession,
46
47
  archiveSession,
47
48
  deleteSession,
@@ -63,6 +63,7 @@ import {
63
63
  } from '../conversation/history-index-state.js';
64
64
  import { retireConversationHistoryIndex } from '../conversation/history-index.js';
65
65
  import { ensureSessionConfigFile, saveSessionConfig, loadSessionConfig } from './session-config.js';
66
+ import { ConversationStore } from '../conversation/persist.js';
66
67
  import { repairSessionStore } from './recovery.js';
67
68
  import {
68
69
  addOrUpdateManifestSession,
@@ -529,8 +530,13 @@ export function createSessionFromSpec(yeaftDir, spec, options = {}) {
529
530
  if (!name) throw new SessionCrudError('invalid_name', null, 'group name required');
530
531
 
531
532
  const callerRoster = Array.isArray(input.roster) ? input.roster.slice() : [];
532
- const fallbackVpId = callerRoster.length > 0 ? null : preferDefaultVp(scanSortedVpIds(libDir));
533
- const roster = callerRoster.length > 0 ? callerRoster : (fallbackVpId ? [fallbackVpId] : []);
533
+ const preserveEmptyRoster = options.preserveEmptyRoster === true && Array.isArray(input.roster);
534
+ const fallbackVpId = callerRoster.length > 0 || preserveEmptyRoster
535
+ ? null
536
+ : preferDefaultVp(scanSortedVpIds(libDir));
537
+ const roster = callerRoster.length > 0 || preserveEmptyRoster
538
+ ? callerRoster
539
+ : (fallbackVpId ? [fallbackVpId] : []);
534
540
  // Validate every member up-front so we fail before touching fs.
535
541
  for (const vpId of roster) {
536
542
  if (isReservedVpId(vpId)) {
@@ -593,6 +599,62 @@ export function createSessionFromSpec(yeaftDir, spec, options = {}) {
593
599
  return meta;
594
600
  }
595
601
 
602
+ /**
603
+ * Create an independent Session from an existing Session's durable state.
604
+ * Project membership and server-owned asset storage intentionally remain with
605
+ * their existing owners; message payloads and their references are preserved.
606
+ */
607
+ export function copySession(yeaftDir, sourceSessionId, options = {}) {
608
+ const sourceYeaftDir = resolveSessionYeaftDir(yeaftDir, sourceSessionId);
609
+ const source = requireSession(sourceYeaftDir, sourceSessionId);
610
+ let sourceMeta;
611
+ try {
612
+ sourceMeta = source.getMeta();
613
+ } finally {
614
+ source.close();
615
+ }
616
+
617
+ const requestedName = String(options.name || '').trim();
618
+ const name = requestedName || `${sourceMeta.name} copy`;
619
+ const sourceConfig = loadSessionConfig(sourceYeaftDir, sourceSessionId);
620
+ const copied = createSessionFromSpec(sourceYeaftDir, {
621
+ name,
622
+ roster: Array.isArray(sourceMeta.roster) ? sourceMeta.roster : [],
623
+ defaultVpId: sourceMeta.defaultVpId || null,
624
+ workDir: sourceMeta.workDir || '',
625
+ }, { ...options, preserveEmptyRoster: true });
626
+
627
+ try {
628
+ // Unlike ordinary Session creation, cloning is transactional: silently
629
+ // dropping a source override would make the copy behave differently.
630
+ saveSessionConfig(sourceYeaftDir, copied.id, sourceConfig);
631
+ const target = requireSession(sourceYeaftDir, copied.id);
632
+ try {
633
+ const targetMeta = target.getMeta();
634
+ target.saveMeta({
635
+ ...targetMeta,
636
+ announcement: sourceMeta.announcement || '',
637
+ metadataUpdatedAt: new Date().toISOString(),
638
+ });
639
+ } finally {
640
+ target.close();
641
+ }
642
+
643
+ const transcript = new ConversationStore(sourceYeaftDir);
644
+ const { copiedCount } = transcript.copySession(sourceSessionId, copied.id);
645
+ return { ...requireSessionMeta(sourceYeaftDir, copied.id), copiedMessageCount: copiedCount };
646
+ } catch (error) {
647
+ // A partial clone must never appear as a successful copy.
648
+ deleteSession(sourceYeaftDir, copied.id, options);
649
+ throw error;
650
+ }
651
+ }
652
+
653
+ function requireSessionMeta(yeaftDir, sessionId) {
654
+ const handle = requireSession(yeaftDir, sessionId);
655
+ try { return handle.getMeta(); } finally { handle.close(); }
656
+ }
657
+
596
658
  /**
597
659
  * (A.2) Rename — updates meta.name; preserves everything else.
598
660
  */