@yeaft/webchat-agent 0.1.691 → 0.1.697

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.691",
3
+ "version": "0.1.697",
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
@@ -545,25 +545,29 @@ export class Engine {
545
545
  }
546
546
 
547
547
  /**
548
- * Build the system prompt with memory, compact summary, skill content,
549
- * and (Phase 8 wire-up) Layer-A scope summaries.
548
+ * Build the system prompt with the AMS-rendered Memory block, the
549
+ * Active Scope block, and skill content. The legacy multi-path
550
+ * Memory injection (FTS-formatted + AMS snapshot + Layer-A summaries +
551
+ * userProfile + coreMemory) was retired in DESIGN-PROMPT v1; callers
552
+ * now thread a single `memoryInjection` string composed upstream from
553
+ * the AMS snapshot.
550
554
  *
551
555
  * Routes through `buildWorkerPrompt`, which:
552
556
  * - Lays in the persona-as-identity block (or Yeaft identity fallback)
553
- * - Concatenates Layer A summaries (`user/group/vp/summary.md`)
554
- * - Reserves Layer B / C / D placeholders for future wiring (router
555
- * preselected memory, task scope, turn scope)
557
+ * - Adds the Memory section (passed in as `memoryInjection`)
558
+ * - Adds the structured Active Scope block (`activeScope`)
559
+ * - Forwards optional `taskCtx` for the legacy task-context sub-block
556
560
  *
557
- * @param {{ profile?: string, entries?: object[] }} [memory]
558
- * @param {string} [compactSummary]
559
- * @param {string} [prompt]user prompt (for skill relevance matching)
560
- * @param {string} [memoryInjection] — prebuilt memory block from preflow
561
- * @param {string} [userProfile] — user profile string
562
- * @param {object} [vpPersona]
563
- * @param {{user?:string, group?:string, vp?:string}} [summaries]
561
+ * @param {object} args
562
+ * @param {string} args.prompt — user prompt (for skill relevance matching)
563
+ * @param {string} args.memoryInjectionprebuilt Memory block from AMS
564
+ * @param {object} [args.vpPersona]
565
+ * @param {object} [args.activeScope] — DESIGN-PROMPT §3 ④ structured scope summary
566
+ * @param {string} [args.groupAnnouncement]
567
+ * @param {object} [args.taskCtx] — legacy task-context sub-block (optional)
564
568
  * @returns {string}
565
569
  */
566
- #buildSystemPrompt(memory, compactSummary, prompt, memoryInjection, userProfile, vpPersona, summaries, groupAnnouncement) {
570
+ #buildSystemPrompt({ prompt, memoryInjection, vpPersona, activeScope, groupAnnouncement, taskCtx } = {}) {
567
571
  // Get relevant skill content if SkillManager is wired
568
572
  let skillContent = '';
569
573
  if (this.#skillManager && prompt) {
@@ -578,21 +582,16 @@ export class Engine {
578
582
  return buildWorkerPrompt({
579
583
  language: this.#config.language || 'en',
580
584
  toolNames,
581
- memory,
582
585
  memoryInjection,
583
- compactSummary,
584
586
  skillContent,
585
- userProfile,
586
587
  vpPersona,
587
- summaries,
588
+ activeScope,
588
589
  groupAnnouncement,
590
+ taskCtx,
589
591
  // Worker-shape harness is descriptive metadata for human inspection;
590
592
  // production prompts skip it to save tokens. Re-enable via env when
591
593
  // diagnosing prompt structure issues.
592
594
  includeShape: process.env.UNIFY_PROMPT_INCLUDE_SHAPE === '1',
593
- // task-334f: memory_trace tool is now registered (49 → 51 tools), so
594
- // unlock the core_memory meta-line behind 334e's feature flag.
595
- memoryTraceAvailable: true,
596
595
  });
597
596
  }
598
597
 
@@ -931,16 +930,18 @@ export class Engine {
931
930
  */
932
931
  async *#runQuery({ prompt, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan, groupAnnouncement }) {
933
932
 
934
- // ─── Pre-query: FTS5 Memory Recall + Compact Summary ──
935
- // Memory feed comes from two places:
936
- // (a) FTS5 pre-flow recall (#recallMemory groups/pre-flow.js
937
- // memory/preflow.js) per-turn scoped recall
938
- // (b) AMS snapshot (resident summaries + onDemand FTS hits) appended
939
- // below
933
+ // ─── Pre-query: FTS5 Memory Recall + AMS snapshot ─────
934
+ // Memory has a SINGLE render outlet now (DESIGN-PROMPT §3 ③):
935
+ // 1. FTS5 pre-flow recall produces a list of segments;
936
+ // 2. those segments are pushed into AMS OnDemand;
937
+ // 3. AMS renders a budget-aware snapshot (Resident + Recent +
938
+ // OnDemand) — that snapshot IS `memoryInjection`.
939
+ // The legacy second path (`recallResult.formatted` concatenated
940
+ // directly into `memoryInjection`) was a duplicate render of the
941
+ // same segments AMS would also surface, so it's gone.
940
942
  let memoryInjection = '';
941
943
  let recallEntryCount = 0;
942
944
 
943
- // FTS5 recall: append per-turn scoped hits to memory injection
944
945
  const recallResult = await this.#recallMemory(prompt, {
945
946
  groupId,
946
947
  vpId: vpPersona && typeof vpPersona === 'object' && typeof vpPersona.vpId === 'string'
@@ -950,26 +951,16 @@ export class Engine {
950
951
  ? inboundEnvelope.featureId
951
952
  : undefined,
952
953
  });
953
- if (recallResult && recallResult.formatted) {
954
- memoryInjection = memoryInjection
955
- ? memoryInjection + '\n\n' + recallResult.formatted
956
- : recallResult.formatted;
957
- recallEntryCount = recallResult.entries.length;
958
- }
959
-
960
- if (memoryInjection) {
954
+ recallEntryCount = recallResult && Array.isArray(recallResult.entries)
955
+ ? recallResult.entries.length
956
+ : 0;
957
+ if (recallEntryCount > 0) {
961
958
  yield { type: 'recall', entryCount: recallEntryCount, cached: false };
962
959
  }
963
960
 
964
- const compactSummary = this.#getCompactSummary();
965
- const userProfile = recallResult?.profile || '';
966
-
967
- // Phase 8 wire-up — Layer A scope summaries
968
- // Load `summary.md` for the user / addressed group / addressed VP from
969
- // the scoped memory tree (DESIGN.md §2). This is the rolling synopsis a
970
- // dream tick maintains; we surface it to the worker prompt so the LLM
971
- // has cheap, persistent context without paying the recall cost on every
972
- // turn. Failures are non-fatal (cold-start / no memory dir).
961
+ // Layer-A summaries — same scopes AMS Resident will surface, loaded
962
+ // here so we can pass them into #prepareAms. (Rolling per-scope
963
+ // synopsis maintained by the dream tick.) Failures are non-fatal.
973
964
  const summaries = await this.#loadLayerASummaries({
974
965
  groupId,
975
966
  vpId: vpPersona && typeof vpPersona === 'object' && typeof vpPersona.vpId === 'string'
@@ -979,12 +970,11 @@ export class Engine {
979
970
 
980
971
  // ─── AMS: populate + snapshot ───────────────────────────────
981
972
  // Group-keyed and persisted across session deactivation. Each turn:
982
- // (a) resident layer is rebuilt from <scope>/summary.md (the
983
- // summaries already loaded above are the same scopes, so
984
- // reuse them);
973
+ // (a) resident layer is rebuilt from <scope>/summary.md;
985
974
  // (b) onDemand is replaced with this turn's FTS hits;
986
- // (c) we render a budget-aware snapshot block and append it to
987
- // memoryInjection. Adjust runs post-turn (see end_turn below).
975
+ // (c) we render a budget-aware snapshot block this is the SOLE
976
+ // Memory section in the system prompt. Adjust runs post-turn
977
+ // (see end_turn below).
988
978
  const ownVpIdForAms = vpPersona && typeof vpPersona === 'object'
989
979
  && typeof vpPersona.vpId === 'string'
990
980
  ? vpPersona.vpId
@@ -1000,15 +990,74 @@ export class Engine {
1000
990
  recallEntries: recallResult ? (recallResult.entries || []) : [],
1001
991
  });
1002
992
  if (amsContext && amsContext.snapshotBlock) {
1003
- memoryInjection = memoryInjection
1004
- ? memoryInjection + '\n\n' + amsContext.snapshotBlock
1005
- : amsContext.snapshotBlock;
993
+ memoryInjection = amsContext.snapshotBlock;
1006
994
  }
1007
995
 
1008
- const systemPrompt = this.#buildSystemPrompt(undefined, compactSummary, prompt, memoryInjection, userProfile, vpPersona, summaries, groupAnnouncement);
996
+ // ─── Active Scope (DESIGN-PROMPT §3 ④) ──────────────────────
997
+ // Structured per-turn scope summary: feature + group + vp + envelope
998
+ // routing info. Long-form scope content lives in AMS — this block
999
+ // carries only IDs + tiny labels. featureId is allowed to be null
1000
+ // (T4 Scope Tagging is a placeholder; not every turn lives in a
1001
+ // feature — DESIGN-PROMPT §5.1).
1002
+ const activeScope = {
1003
+ featureId: featureIdForAms || null,
1004
+ featureTitle: typeof inboundEnvelope === 'object' && inboundEnvelope
1005
+ && typeof inboundEnvelope.featureTitle === 'string'
1006
+ ? inboundEnvelope.featureTitle
1007
+ : '',
1008
+ groupId: groupId || '',
1009
+ vpId: ownVpIdForAms || '',
1010
+ envelope: inboundEnvelope || null,
1011
+ };
1012
+
1013
+ const systemPrompt = this.#buildSystemPrompt({
1014
+ prompt,
1015
+ memoryInjection,
1016
+ vpPersona,
1017
+ activeScope,
1018
+ groupAnnouncement,
1019
+ // taskCtx is not currently wired by the query loop. The legacy
1020
+ // task-context sub-block (renderTaskCtx, task-334e/334n contract) is
1021
+ // retained behind a feature flag for callers that still build it
1022
+ // upstream — it'll be folded into Active Scope or retired in a
1023
+ // dedicated PR alongside the task-334n cleanup.
1024
+ taskCtx: undefined,
1025
+ });
1009
1026
 
1010
- // Build conversation: existing messages + new user message
1027
+ // ─── Compact summary as messages-array head (DESIGN-PROMPT §4.3)
1028
+ // The previous code placed the compact summary inside the system
1029
+ // prompt; that broke prompt-cache hit-rate (any compact update
1030
+ // invalidated the entire system) and conflated identity/rules with
1031
+ // dialogue history. The compact summary is the product of compressing
1032
+ // older turns, so it belongs at the head of the messages array.
1033
+ //
1034
+ // Note: this is a separate mechanism from `history-compact.js`'s
1035
+ // `_compactSummary`-tagged user message. They never collide:
1036
+ // • THIS path injects a `<conversation_summary>` pair on every
1037
+ // query when conversationStore.readCompactSummary() returns text
1038
+ // (i.e. when a previous T1 run wrote one to disk). Engine reads,
1039
+ // does not produce.
1040
+ // • history-compact.js#compactHistory rewrites the in-memory
1041
+ // `messages` array, replacing cold messages with a single
1042
+ // `_compactSummary`-tagged user message. That path runs at a
1043
+ // different layer (web-bridge during a manual /compact) and never
1044
+ // touches `compactMessages` here.
1045
+ // The two would only overlap if a tagged `_compactSummary` user
1046
+ // message also matched the `<conversation_summary>` template — they
1047
+ // don't, so duplication is impossible by construction.
1048
+ const compactSummaryRaw = this.#getCompactSummary();
1049
+ const compactSummary = typeof compactSummaryRaw === 'string'
1050
+ ? compactSummaryRaw.trim() : '';
1051
+ const compactMessages = compactSummary
1052
+ ? [
1053
+ { role: 'user', content: `<conversation_summary>\n${compactSummary}\n</conversation_summary>` },
1054
+ { role: 'assistant', content: 'Acknowledged.' },
1055
+ ]
1056
+ : [];
1057
+
1058
+ // Build conversation: optional compact head + existing messages + new user message
1011
1059
  const conversationMessages = [
1060
+ ...compactMessages,
1012
1061
  ...messages,
1013
1062
  { role: 'user', content: prompt },
1014
1063
  ];
@@ -1,28 +1,35 @@
1
1
  /**
2
- * memory/budget.js — DESIGN-H2-AMS §5.2.
2
+ * memory/budget.js — DESIGN-PROMPT §3 ③ Memory.
3
3
  *
4
- * Memory budget = `min(50_000, modelMaxContext * 0.10)`.
4
+ * Memory budget = `min(100_000, modelMaxContext * 0.20)`.
5
5
  *
6
6
  * Then split across the three AMS layers (resident / recent / onDemand)
7
- * with a configurable ratio. The defaults are tuned for ~200k context
8
- * models (Claude / GPT-5):
7
+ * with a configurable ratio. The defaults are tuned per DESIGN-PROMPT §3
8
+ * (Resident gets the largest share because UserProfile + CoreMemory
9
+ * collapse into Resident now):
9
10
  *
10
- * resident 40% → 20k (all relevant scope summaries)
11
- * recent 25% → 12.5k (LRU of recently-used segments)
12
- * onDemand 35% → 17.5k (this turn's FTS recall)
11
+ * resident 60% → 24k of a 40k pool (Layer-A summaries +
12
+ * UserProfile + CoreMemory pinned)
13
+ * recent 15% → 6k (LRU of recently-used segments)
14
+ * onDemand 25% → 10k (this turn's FTS recall)
15
+ *
16
+ * Concrete budgets for common models:
17
+ * 200K context → 40K total (20% × 200K)
18
+ * 1M context → 100K total (capped)
19
+ * 128K context → 25.6K total
13
20
  *
14
21
  * Token counting here is approximate (chars / 4) — accurate enough for
15
22
  * budget enforcement. The engine has a real tokenizer for prompt
16
23
  * assembly; budget here is a guard rail, not the source of truth.
17
24
  */
18
25
 
19
- export const ABSOLUTE_CAP = 50_000;
20
- export const MODEL_FRACTION = 0.10;
26
+ export const ABSOLUTE_CAP = 100_000;
27
+ export const MODEL_FRACTION = 0.20;
21
28
 
22
29
  export const DEFAULT_RATIO = {
23
- resident: 0.40,
24
- recent: 0.25,
25
- onDemand: 0.35,
30
+ resident: 0.60,
31
+ recent: 0.15,
32
+ onDemand: 0.25,
26
33
  };
27
34
 
28
35
  /**
package/unify/prompts.js CHANGED
@@ -7,22 +7,24 @@
7
7
  * Template files from agent/unify/templates/ are loaded once at startup
8
8
  * and used to enrich the system prompt beyond the hardcoded fallbacks.
9
9
  *
10
- * Phase 2 additions:
11
- * - Memory section (recalled segments via H2-AMS pre-flow)
12
- * - Compact summary section (conversation history summary)
10
+ * Concept layering (DESIGN-PROMPT §3):
11
+ * Identity — VP persona body (or Yeaft fallback)
12
+ * Rules — group announcement, date, mode template, tools,
13
+ * tool-guidance, skills, common rules
14
+ * ③ Memory — single block produced upstream by the AMS render
15
+ * outlet and threaded through here as `memoryInjection`
16
+ * ④ Active Scope — structured per-turn scope summary
17
+ * (feature / group / vp / envelope IDs)
13
18
  *
14
- * Memory injection (H2-AMS):
15
- * - `memoryInjection` carries prebuilt FTS-recall text from
16
- * `memory/preflow.js` over the relevant scopes (user/group/vp/feature/
17
- * global). Engine passes it every turn after preflow runs.
18
- *
19
- * Reference: yeaft-unify-system-prompt-budget.md — Static + Dynamic + Context layers
19
+ * The compact summary, user_profile, and core_memory blocks that used to
20
+ * live inside the system prompt are GONE. Compact summary is now part of
21
+ * the messages timeline; user_profile + core_memory have been folded into
22
+ * AMS Resident.
20
23
  */
21
24
 
22
25
  import { readFileSync, existsSync } from 'fs';
23
26
  import { join, dirname } from 'path';
24
27
  import { fileURLToPath } from 'url';
25
- import { homedir } from 'os';
26
28
 
27
29
  // ─── Template Loading (one-time at startup) ──────────────────────
28
30
 
@@ -169,18 +171,13 @@ const PROMPTS = {
169
171
  date: (d) => `Date: ${d}`,
170
172
  dream: 'You are in dream mode. Reflect on past conversations and consolidate memories.',
171
173
  tools: (names) => `Available tools: ${names}`,
172
- memoryHeader: '## User Memory',
173
- profileHeader: '### User Profile',
174
- recalledHeader: '### Recalled Memories',
175
- compactHeader: '## Conversation History Summary',
176
- // task-334e — new section headers
174
+ // task-334e task-context section header (sub-block of Active Scope)
177
175
  taskCtxHeader: '## task_ctx',
178
176
  taskCtxRelatedHeader: '### related tasks',
179
177
  taskCtxSummaryReminder: (min, count) =>
180
178
  `💡 ${min}min since last summary (+${count} new messages). Consider calling \`task_summary_post\`.`,
181
- userProfileHeader: '## user_profile',
182
- coreMemoryHeader: '## core_memory',
183
- coreMemoryMeta: 'To open the original message behind any entry above, call `memory_trace`.',
179
+ // DESIGN-PROMPT §3 ④ — Active Scope header
180
+ activeScopeHeader: '## active_scope',
184
181
  vpPersonaIntro: (name, role) =>
185
182
  `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.`,
186
183
  },
@@ -189,18 +186,13 @@ const PROMPTS = {
189
186
  date: (d) => `日期:${d}`,
190
187
  dream: '你处于梦境模式。回顾过去的对话,整理和巩固记忆。',
191
188
  tools: (names) => `可用工具:${names}`,
192
- memoryHeader: '## 用户记忆',
193
- profileHeader: '### 用户画像',
194
- recalledHeader: '### 相关记忆',
195
- compactHeader: '## 对话历史摘要',
196
- // task-334e — new section headers
189
+ // task-334e — task-context section header (sub-block of Active Scope)
197
190
  taskCtxHeader: '## task_ctx',
198
191
  taskCtxRelatedHeader: '### 相关任务',
199
192
  taskCtxSummaryReminder: (min, count) =>
200
193
  `💡 距上次 summary 已过 ${min}min,新增 ${count} 条消息,建议调用 \`task_summary_post\`。`,
201
- userProfileHeader: '## user_profile',
202
- coreMemoryHeader: '## core_memory',
203
- coreMemoryMeta: '如需原始 message,调 `memory_trace`。',
194
+ // DESIGN-PROMPT §3 ④ — Active Scope header
195
+ activeScopeHeader: '## active_scope',
204
196
  vpPersonaIntro: (name, role) =>
205
197
  `你就是 **${name}**${role ? `(${role})` : ''}。请以 ${name} 的第一人称发言;不要自称 "Yeaft" 或泛指的 AI 助手。下面的文字是你的身份、专业方向与判断风格。`,
206
198
  },
@@ -217,19 +209,18 @@ export const SUPPORTED_LANGUAGES = Object.keys(PROMPTS);
217
209
  * — only `mode === 'dream'` triggers the dream-mode template (used by background
218
210
  * memory maintenance); all other values fall through to unified mode.
219
211
  *
220
- * Prompt structure:
221
- * 1. Core identity (from template or fallback)
222
- * 2. Date metadata
223
- * 3. Mode-specific behavioral instructions (unified, or dream)
224
- * 4. Tool list + tool guidance (from template)
225
- * 5. Skills section
226
- * 6. Memory section
227
- * 7. Compact summary section
228
- * 8. Task context section (task-334e §Δ24.5 + §Δ27.3 + §Δ31.4)
229
- * 9. User profile section (task-334e §Δ29.3 stub)
230
- * 10. Core memory section (task-334e §Δ24.5)
212
+ * Prompt structure (DESIGN-PROMPT §3):
213
+ * Identity — Core identity (persona or Yeaft fallback)
214
+ * Rules — Group announcement, date, mode, tools, guidance, skills
215
+ * Memory — Single block produced by the AMS render outlet
216
+ * (callers pass it as `memoryInjection`).
217
+ * Active Scope — Structured per-turn scope summary
218
+ * (feature / group / vp / envelope IDs).
219
+ * (Task context lives inside Active Scope; the previous standalone
220
+ * user_profile / core_memory blocks are gone those signals now
221
+ * arrive through AMS Resident.)
231
222
  *
232
- * task-334e params:
223
+ * task-334e taskCtx is preserved as a sub-block of Active Scope:
233
224
  * @param {object} [taskCtx] — per-task context
234
225
  * @param {string} [taskCtx.taskId]
235
226
  * @param {string} [taskCtx.currentVpId] — used for ACL + initiator check
@@ -244,29 +235,24 @@ export const SUPPORTED_LANGUAGES = Object.keys(PROMPTS);
244
235
  * @param {number} [taskCtx.summaryReminder.lastSummaryAt] — epoch ms (0/missing = never)
245
236
  * @param {number} [taskCtx.summaryReminder.now] — override clock (tests), default Date.now()
246
237
  *
247
- * @param {string} [userProfile] explicit profile content (334l path);
248
- * when omitted we read `~/.yeaft/user/profile.json` `{ content }` as stub.
249
- * @param {{ entries?: Array<{body:string, shard?:string}>, max?: number }} [coreMemory]
250
- * recalled memory entries; we render top-7 bodies + meta line.
251
- *
252
- * @param {boolean} [memoryTraceAvailable=false] — feature flag gating the
253
- * "call memory_trace" meta line in the core_memory block. Defaults to
254
- * false so we don't point VPs at an unimplemented tool (prev-3 Nit-2 /
255
- * PM-approved Option A). 334f will flip this to `true` from session.js
256
- * once `memory_trace` ships; this slice stays decoupled from session.js.
238
+ * Active Scope params (DESIGN-PROMPT §3 ):
239
+ * @param {object} [activeScope] structured scope summary for this turn
240
+ * @param {string|null} [activeScope.featureId] currently active feature, or null
241
+ * @param {string} [activeScope.featureTitle] short title for human display
242
+ * @param {string} [activeScope.groupId]
243
+ * @param {string} [activeScope.vpId]
244
+ * @param {object} [activeScope.envelope] inbound routing info (sender, intent)
257
245
  *
258
246
  * @param {{
259
247
  * language?: string,
260
248
  * mode?: string,
261
249
  * toolNames?: string[],
262
- * memory?: { profile?: string, entries?: object[] },
263
250
  * memoryInjection?: string,
264
- * compactSummary?: string,
265
251
  * skillContent?: string,
266
252
  * taskCtx?: object,
267
- * userProfile?: string,
268
- * coreMemory?: object,
269
- * memoryTraceAvailable?: boolean,
253
+ * activeScope?: object,
254
+ * vpPersona?: object,
255
+ * groupAnnouncement?: string,
270
256
  * }} params
271
257
  * @returns {string}
272
258
  */
@@ -274,14 +260,10 @@ export function buildSystemPrompt({
274
260
  language = 'en',
275
261
  mode,
276
262
  toolNames = [],
277
- memory,
278
263
  memoryInjection,
279
- compactSummary,
280
264
  skillContent,
281
265
  taskCtx,
282
- userProfile,
283
- coreMemory,
284
- memoryTraceAvailable = false,
266
+ activeScope,
285
267
  vpPersona,
286
268
  groupAnnouncement = '',
287
269
  } = {}) {
@@ -351,31 +333,29 @@ export function buildSystemPrompt({
351
333
  parts.push(skillContent);
352
334
  }
353
335
 
354
- // ─── 6. Memory Section ─────────────────────────────────
355
- // FTS5 pre-flow recall + AMS snapshot are concatenated upstream by the
356
- // engine into a single `memoryInjection` block. The legacy entries-based
357
- // memory.profile / memory.entries shape was retired in the H2-AMS rip.
336
+ // ─── 6. Memory Section (DESIGN-PROMPT §3 ③) ────────────
337
+ // The Memory section has a SINGLE render outlet. Callers compose the
338
+ // block upstream by rendering the AMS snapshot (Resident + Recent +
339
+ // OnDemand) and passing the result here as `memoryInjection`. The
340
+ // legacy multi-path injection (FTS-formatted + AMS snapshot +
341
+ // renderLayerASummaries + renderUserProfile + renderCoreMemory) was
342
+ // retired in DESIGN-PROMPT v1: it produced 2-3× duplicated content
343
+ // for the same `summary.md` payload.
358
344
  if (memoryInjection && memoryInjection.trim()) {
359
345
  parts.push(memoryInjection.trim());
360
346
  }
361
347
 
362
- // ─── 7. Compact Summary Section ────────────────────────
363
- if (compactSummary) {
364
- parts.push(`${lang.compactHeader}\n${compactSummary}`);
365
- }
348
+ // ─── 7. Active Scope (DESIGN-PROMPT §3 ④) ──────────────
349
+ // Structured per-turn scope summary. taskCtx is rendered as a
350
+ // sub-block of Active Scope (when supplied), and the new
351
+ // feature/group/vp/envelope identifiers are rendered as a leading
352
+ // line.
353
+ const activeScopeBlock = renderActiveScope(activeScope, lang);
354
+ if (activeScopeBlock) parts.push(activeScopeBlock);
366
355
 
367
- // ─── 8. Task Context Section (task-334e §Δ24.5 / §Δ27.3 / §Δ31.4) ─
368
356
  const taskCtxBlock = renderTaskCtx(taskCtx, lang);
369
357
  if (taskCtxBlock) parts.push(taskCtxBlock);
370
358
 
371
- // ─── 9. User Profile Section (task-334e §Δ29.3 stub) ───
372
- const profileBlock = renderUserProfile(userProfile, lang);
373
- if (profileBlock) parts.push(profileBlock);
374
-
375
- // ─── 10. Core Memory Section (task-334e §Δ24.5) ────────
376
- const coreMemBlock = renderCoreMemory(coreMemory, lang, memoryTraceAvailable);
377
- if (coreMemBlock) parts.push(coreMemBlock);
378
-
379
359
  return parts.join('\n\n');
380
360
  }
381
361
 
@@ -418,7 +398,6 @@ function renderVpPersona(vpPersona, lang) {
418
398
  const DEFAULT_TASK_MEMORY_TOP = 5;
419
399
  const DEFAULT_RELATED_TASK_TOP = 3;
420
400
  const DEFAULT_RELATED_TASK_MEMORY_TOP = 2;
421
- const DEFAULT_CORE_MEMORY_TOP = 7;
422
401
  // task-334n §Δ31.4 — tightened reminder gate:
423
402
  // (a) currentVpId === initiatorVpId
424
403
  // (b) task.members.length >= 2 (multi-VP only)
@@ -559,94 +538,114 @@ function renderSummaryReminder(taskCtx, lang) {
559
538
  }
560
539
 
561
540
  /**
562
- * Render `## user_profile` block. If the caller passed an explicit string,
563
- * we use it verbatim (that's the 334l path). Otherwise we stub-read from
564
- * `~/.yeaft/user/profile.json` (`{ content: "..." }`) per §Δ29.3. Any IO
565
- * error is swallowed this is best-effort context, not critical path.
541
+ * Render `## active_scope` block (DESIGN-PROMPT §3 ④).
542
+ *
543
+ * Active Scope is a structured, deterministic, bounded block telling the
544
+ * LLM what scope the current turn lives in. It is NOT memory; long-form
545
+ * scope content (decisions, history) flows through AMS — Active Scope
546
+ * carries only IDs + tiny labels.
547
+ *
548
+ * Schema:
549
+ * ## active_scope
550
+ * feature: <featureId> "<title>" (omitted when null/empty)
551
+ * group: <groupId> (omitted when missing)
552
+ * vp: <vpId> (omitted when missing)
553
+ * envelope: from=<sender> intent=<intent> (omitted when no envelope)
554
+ *
555
+ * Returns '' when the input has no useful field — we don't emit an empty
556
+ * header. featureId is allowed to be `null` (DESIGN-PROMPT §5.1 — T4
557
+ * Scope Tagging is a placeholder; not every turn lives in a feature).
558
+ *
559
+ * @param {object} [activeScope]
560
+ * @param {string|null} [activeScope.featureId]
561
+ * @param {string} [activeScope.featureTitle]
562
+ * @param {string} [activeScope.groupId]
563
+ * @param {string} [activeScope.vpId]
564
+ * @param {object} [activeScope.envelope] inbound routing summary
565
+ * @param {object} lang
566
+ * @returns {string}
566
567
  */
567
- function renderUserProfile(userProfile, lang) {
568
- let content = '';
569
- if (typeof userProfile === 'string' && userProfile.trim()) {
570
- content = userProfile.trim();
571
- } else if (userProfile == null) {
572
- content = readUserProfileStub();
573
- }
574
- if (!content) return '';
575
- return `${lang.userProfileHeader}\n${content}`;
576
- }
568
+ function renderActiveScope(activeScope, lang) {
569
+ if (!activeScope || typeof activeScope !== 'object') return '';
577
570
 
578
- function readUserProfileStub() {
579
- try {
580
- const path = join(homedir(), '.yeaft', 'user', 'profile.json');
581
- if (!existsSync(path)) return '';
582
- const raw = readFileSync(path, 'utf8');
583
- const parsed = JSON.parse(raw);
584
- if (parsed && typeof parsed.content === 'string') return parsed.content.trim();
585
- return '';
586
- } catch {
587
- // File missing, unreadable, malformed JSON, or non-string content.
588
- // Stub is best-effort — fall through silently.
589
- return '';
571
+ const lines = [];
572
+ const feature = typeof activeScope.featureId === 'string' && activeScope.featureId.trim()
573
+ ? activeScope.featureId.trim()
574
+ : null;
575
+ if (feature) {
576
+ // Escape embedded `"` in featureTitle so a title like `Onboard "v2"` does
577
+ // not produce a malformed `feature: f1 "Onboard "v2""` line. Titles come
578
+ // from user / agent input — assume nothing.
579
+ const title = typeof activeScope.featureTitle === 'string' && activeScope.featureTitle.trim()
580
+ ? ` "${activeScope.featureTitle.trim().replace(/"/g, '\\"')}"`
581
+ : '';
582
+ lines.push(`feature: ${feature}${title}`);
590
583
  }
584
+ const group = typeof activeScope.groupId === 'string' && activeScope.groupId.trim()
585
+ ? activeScope.groupId.trim()
586
+ : '';
587
+ if (group) lines.push(`group: ${group}`);
588
+
589
+ const vp = typeof activeScope.vpId === 'string' && activeScope.vpId.trim()
590
+ ? activeScope.vpId.trim()
591
+ : '';
592
+ if (vp) lines.push(`vp: ${vp}`);
593
+
594
+ const envLine = renderEnvelopeLine(activeScope.envelope);
595
+ if (envLine) lines.push(`envelope: ${envLine}`);
596
+
597
+ if (lines.length === 0) return '';
598
+
599
+ return `${lang.activeScopeHeader}\n${lines.join('\n')}`;
591
600
  }
592
601
 
593
602
  /**
594
- * Render `## core_memory` block with recall top-7 bodies + (optional) meta line.
603
+ * Render a one-line envelope summary. Pulls the small set of routing
604
+ * fields we surface to the LLM (sender, intent, originating user) and
605
+ * leaves the rest in AMS. Returns '' when the envelope carries no
606
+ * useful signal.
595
607
  *
596
- * Accepts `{ entries: [{body,shard}], max?: number }`. Never renders
597
- * `sourceRef`. The "call memory_trace" meta line is gated by
598
- * `memoryTraceAvailable` (prev-3 Nit-2 / PM-approved Option A): when the
599
- * `memory_trace` tool is not yet implemented (334f), we omit the meta line
600
- * entirely so the LLM does not try to call a non-existent tool. 334f will
601
- * flip the flag to `true` when it wires session.js.
608
+ * @param {object|null|undefined} envelope
609
+ * @returns {string}
602
610
  */
603
- function renderCoreMemory(coreMemory, lang, memoryTraceAvailable) {
604
- if (!coreMemory || typeof coreMemory !== 'object') return '';
605
- const entries = Array.isArray(coreMemory.entries) ? coreMemory.entries : [];
606
- if (entries.length === 0) return '';
607
- const max = Number.isFinite(coreMemory.max) && coreMemory.max > 0
608
- ? Math.floor(coreMemory.max)
609
- : DEFAULT_CORE_MEMORY_TOP;
610
-
611
- const lines = [lang.coreMemoryHeader];
612
- let shown = 0;
613
- for (const e of entries) {
614
- if (shown >= max) break;
615
- const body = typeof e?.body === 'string' ? e.body.trim() : '';
616
- if (!body) continue;
617
- const shard = typeof e?.shard === 'string' && e.shard.trim() ? e.shard.trim() : 'general';
618
- lines.push(`- [${shard}] ${body}`);
619
- shown += 1;
620
- }
621
- if (shown === 0) return '';
622
- if (memoryTraceAvailable) {
623
- lines.push('');
624
- lines.push(lang.coreMemoryMeta);
625
- }
626
- return lines.join('\n');
611
+ function renderEnvelopeLine(envelope) {
612
+ if (!envelope || typeof envelope !== 'object') return '';
613
+ const segments = [];
614
+ const fromVp = typeof envelope.fromVpId === 'string' && envelope.fromVpId.trim()
615
+ ? envelope.fromVpId.trim()
616
+ : (typeof envelope.senderVpId === 'string' ? envelope.senderVpId.trim() : '');
617
+ if (fromVp) segments.push(`from=${fromVp}`);
618
+ const fromUser = typeof envelope.fromUserId === 'string' && envelope.fromUserId.trim()
619
+ ? envelope.fromUserId.trim()
620
+ : '';
621
+ if (fromUser) segments.push(`user=${fromUser}`);
622
+ const intent = typeof envelope.intent === 'string' && envelope.intent.trim()
623
+ ? envelope.intent.trim()
624
+ : '';
625
+ if (intent) segments.push(`intent=${intent}`);
626
+ return segments.join(' ');
627
627
  }
628
628
 
629
629
  // ─── Phase 1: Worker / Router prompt splits ──────────────────────
630
630
  //
631
631
  // DESIGN.md (multi-VP redesign) describes two distinct prompt shapes:
632
632
  //
633
- // • Worker prompt — what a VP sees when it executes a turn. Layered as
634
- // A (identity + summaries) / B (router-preselected memory) / C (task
635
- // scope) / D (turn scope).
636
- // Router prompt what the per-VP Router sees before it decides
637
- // plans[]. Identity-summary layer + recent group state, no task /
638
- // turn-scope detail.
633
+ // • Worker prompt — what a VP sees when it executes a turn. The
634
+ // DESIGN-PROMPT v1 refactor collapsed the previous A/B/C/D layered
635
+ // shape into a single AMS-driven Memory block: AMS Resident now
636
+ // carries Layer-A summaries + UserProfile + CoreMemory, AMS OnDemand
637
+ // carries the per-turn FTS hits. The worker shape that survives is:
638
+ // harness/worker-shape — optional descriptive metadata
639
+ // buildSystemPrompt(...) — ① Identity ② Rules ③ Memory ④ Active Scope
640
+ // optional taskScope/turnScope — caller-provided pass-through strings
641
+ // `renderLayerASummaries` is no longer called inside the worker prompt
642
+ // because AMS already renders the same summaries — calling both was
643
+ // the duplicate-render bug DESIGN-PROMPT §6.1 #2 set out to fix.
639
644
  //
640
- // To stay backwards-compatible with existing callers we KEEP
641
- // `buildSystemPrompt` and treat the two new entry points as thin wrappers
642
- // that:
643
- // 1) compose Layer-A summaries (user / group / vp) into the right
644
- // headed sections, and
645
- // 2) prepend the matching harness/*-shape.md fragment when present.
646
- //
647
- // Subsequent phases will migrate engine.js / router.js to these entry
648
- // points and start filling Layers B / C with the new memory tree. For
649
- // now they exist primarily so tests can pin the contract.
645
+ // Router prompt — what the per-VP Router sees before it decides
646
+ // plans[]. This is a separate, smaller LLM call that does not run
647
+ // AMS, so it still uses `renderLayerASummaries` directly to surface
648
+ // the three Layer-A summaries inline.
650
649
 
651
650
  const LAYER_A_HEADERS = {
652
651
  en: {
@@ -663,8 +662,11 @@ const LAYER_A_HEADERS = {
663
662
 
664
663
  /**
665
664
  * Render Layer A's three rolling summaries (user / group / vp). Each is
666
- * optional; missing or empty strings are skipped. Headers follow the
667
- * `## summary_<scope>` convention so Layer-B/C/D headers don't collide.
665
+ * optional; missing or empty strings are skipped.
666
+ *
667
+ * Used by the Router prompt path only — the Worker prompt path receives
668
+ * the same summaries through AMS Resident (see DESIGN-PROMPT §3 ③) and
669
+ * MUST NOT call this in addition.
668
670
  *
669
671
  * @param {{user?: string, group?: string, vp?: string}} summaries
670
672
  * @param {'en'|'zh'} language
@@ -683,26 +685,22 @@ export function renderLayerASummaries(summaries, language = 'en') {
683
685
  }
684
686
 
685
687
  /**
686
- * Worker prompt entry point (DESIGN.md Phase 1).
688
+ * Worker prompt entry point.
687
689
  *
688
- * Layered output:
689
- * harness/worker-shape — what each layer means (optional fragment)
690
- * Layer A — buildSystemPrompt(...) output (identity + persona + Layer-A
691
- * summaries via `summaries`)
692
- * Layer B — `preselectedMemory` block (router-supplied)
693
- * Layer C — `taskScope` block (active task summary + related-task window)
694
- * Layer D — `turnScope` block (inbound envelope, in-flight turn notes)
690
+ * Output sections (DESIGN-PROMPT §3 layered concepts):
691
+ * harness/worker-shape (optional)descriptive metadata
692
+ * buildSystemPrompt(...) Identity Rules Memory ④ Active Scope
695
693
  *
696
- * Layers B/C/D are passed in as already-rendered strings so this builder
697
- * stays free of memory-store / task-store IO. Phase 2/3 will provide the
698
- * real renderers; for now any caller can stub them.
694
+ * Earlier task-322 / task-334e variants accepted `taskScope` and
695
+ * `turnScope` pass-through strings so callers could append their own
696
+ * scope blocks. DESIGN-PROMPT v1 retired that surface Active Scope is
697
+ * now structured (`activeScope: { featureId, groupId, vpId, envelope }`)
698
+ * and rendered by `buildSystemPrompt` itself. Both pass-through params
699
+ * had zero remaining callers when v1 landed; removing them prevents the
700
+ * "two ways to describe scope" drift §1 set out to eliminate.
699
701
  *
700
702
  * @param {{
701
703
  * language?: 'en'|'zh',
702
- * summaries?: {user?: string, group?: string, vp?: string},
703
- * preselectedMemory?: string,
704
- * taskScope?: string,
705
- * turnScope?: string,
706
704
  * includeShape?: boolean,
707
705
  * ...rest: import('./prompts.js').buildSystemPrompt
708
706
  * }} params
@@ -711,10 +709,6 @@ export function renderLayerASummaries(summaries, language = 'en') {
711
709
  export function buildWorkerPrompt(params = {}) {
712
710
  const {
713
711
  language = 'en',
714
- summaries,
715
- preselectedMemory,
716
- taskScope,
717
- turnScope,
718
712
  includeShape = true,
719
713
  ...rest
720
714
  } = params;
@@ -727,26 +721,9 @@ export function buildWorkerPrompt(params = {}) {
727
721
  if (shape) parts.push(shape);
728
722
  }
729
723
 
730
- // Layer A base + persona + summaries.
724
+ // Identity + Rules + Memory + Active Scope (DESIGN-PROMPT §3).
731
725
  const baseBlock = buildSystemPrompt({ ...rest, language });
732
726
  if (baseBlock) parts.push(baseBlock);
733
- const summaryBlock = renderLayerASummaries(summaries, language);
734
- if (summaryBlock) parts.push(summaryBlock);
735
-
736
- // Layer B — router-preselected memory entries (rendered upstream).
737
- if (typeof preselectedMemory === 'string' && preselectedMemory.trim()) {
738
- parts.push(preselectedMemory.trim());
739
- }
740
-
741
- // Layer C — task scope.
742
- if (typeof taskScope === 'string' && taskScope.trim()) {
743
- parts.push(taskScope.trim());
744
- }
745
-
746
- // Layer D — turn scope (inbound envelope, in-flight turn notes).
747
- if (typeof turnScope === 'string' && turnScope.trim()) {
748
- parts.push(turnScope.trim());
749
- }
750
727
 
751
728
  return parts.join('\n\n');
752
729
  }