@yeaft/webchat-agent 0.1.662 → 0.1.664

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.662",
3
+ "version": "0.1.664",
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/cli.js CHANGED
@@ -619,11 +619,6 @@ async function runREPL(config, args) {
619
619
  process.stderr.write(`[compact] Memory consolidated\n`);
620
620
  }
621
621
  break;
622
- case 'dream_triggered':
623
- if (session.config.debug) {
624
- process.stderr.write(`[dream] Dream cycle triggered (background)\n`);
625
- }
626
- break;
627
622
  case 'fallback':
628
623
  process.stderr.write(`\n[fallback] ${event.from} → ${event.to}: ${event.reason}\n`);
629
624
  break;
@@ -717,11 +712,6 @@ async function runOnce(config, args) {
717
712
  process.stderr.write(`[consolidate] archived=${event.archivedCount}, extracted=${event.extractedCount}\n`);
718
713
  }
719
714
  break;
720
- case 'dream_triggered':
721
- if (args.verbose || args.debug) {
722
- process.stderr.write(`[dream] Dream cycle triggered (background)\n`);
723
- }
724
- break;
725
715
  case 'fallback':
726
716
  process.stderr.write(`\n[fallback] ${event.from} → ${event.to}: ${event.reason}\n`);
727
717
  break;
package/unify/engine.js CHANGED
@@ -416,6 +416,15 @@ export class Engine {
416
416
  const ownVpId = args.ownVpId || null;
417
417
  const ams = this.#amsRegistry.getOrCreate(groupKey, { ownVpId });
418
418
 
419
+ // Prime #adjustRanByGroup from disk-hydrated state on first access:
420
+ // a reactivated group resumes with whatever adjustRanThisSession bit
421
+ // it had on disconnect, so we don't burn a fresh adjust on every
422
+ // reload. Once set true in this session we never clear it.
423
+ if (!this.#adjustRanByGroup.has(groupKey)
424
+ && this.#amsRegistry.adjustRanThisSession(groupKey)) {
425
+ this.#adjustRanByGroup.set(groupKey, true);
426
+ }
427
+
419
428
  // (a) Resident: rebuild from the same scope summaries the worker
420
429
  // prompt is already going to see.
421
430
  const residentEntries = [];
@@ -1456,9 +1465,6 @@ export class Engine {
1456
1465
  if (hookResult.consolidated) {
1457
1466
  yield { type: 'consolidate', archivedCount: 0, extractedCount: 0 };
1458
1467
  }
1459
- if (hookResult.dreamTriggered) {
1460
- yield { type: 'dream_triggered' };
1461
- }
1462
1468
  } else {
1463
1469
  // Legacy path (no yeaftDir → use old behavior)
1464
1470
  this.#persistMessages(prompt, fullResponseText, assistantMsg.toolCalls, groupId);
package/unify/index.js CHANGED
@@ -26,7 +26,6 @@ export { MemoryStore, parseEntry, serializeEntry, MEMORY_KINDS } from './memory/
26
26
 
27
27
  // Phase 5: Advanced features
28
28
  export { KINDS, KIND_PRIORITY, KIND_DESCRIPTIONS, IMPORTANCE_LEVELS, validateEntry, parseScopePath, getAncestorScopes, areScopesRelated } from './memory/types.js';
29
- export { scanEntries, scoreEntry, findStaleEntries, findDuplicateGroups, summarizeScan } from './memory/scan.js';
30
29
  export { runStopHooks } from './stop-hooks.js';
31
30
  export { MCPManager, createMCPManager } from './mcp.js';
32
31
  export { SkillManager, createSkillManager, parseSkill, serializeSkill } from './skills.js';
package/unify/init.js CHANGED
@@ -79,7 +79,6 @@ const SUBDIRS = [
79
79
  'conversation/blobs',
80
80
  'memory/entries',
81
81
  'tasks',
82
- 'dream',
83
82
  'skills',
84
83
  ];
85
84
 
@@ -48,6 +48,7 @@ export const DEFAULT_GROUP_KEY = 'default';
48
48
  * @typedef {object} AmsCacheEntry
49
49
  * @property {ActiveMemorySet} ams
50
50
  * @property {string|null} ownVpId
51
+ * @property {boolean} adjustRanThisSession
51
52
  */
52
53
 
53
54
  /**
@@ -77,12 +78,16 @@ export class AmsRegistry {
77
78
  /**
78
79
  * Resolve the on-disk path for a group's ams.json.
79
80
  *
81
+ * `groupId` is trusted: `nextGroupId()` (groups/ids.js) emits ids matching
82
+ * `grp_[a-z0-9_-]+`, and the single-VP path uses the literal
83
+ * `DEFAULT_GROUP_KEY`. No defensive escaping is needed.
84
+ *
80
85
  * @param {string} groupId
81
86
  * @returns {string}
82
87
  */
83
88
  amsPath(groupId) {
84
- const safe = String(groupId || DEFAULT_GROUP_KEY).replace(/[^A-Za-z0-9._-]/g, '_');
85
- return join(this.yeaftDir, 'memory', 'groups', safe, 'ams.json');
89
+ const key = String(groupId || DEFAULT_GROUP_KEY);
90
+ return join(this.yeaftDir, 'memory', 'groups', key, 'ams.json');
86
91
  }
87
92
 
88
93
  /**
@@ -114,12 +119,40 @@ export class AmsRegistry {
114
119
  const ownVpId = opts.ownVpId || null;
115
120
  const budget = this._budget();
116
121
  const ams = new ActiveMemorySet({ ownVpId, budget });
117
- // Best-effort hydrate from disk.
118
- this._hydrate(key, ams);
119
- this._cache.set(key, { ams, ownVpId });
122
+ const entry = { ams, ownVpId, adjustRanThisSession: false };
123
+ // Best-effort hydrate from disk — populates ams + entry flags.
124
+ this._hydrate(key, entry);
125
+ this._cache.set(key, entry);
120
126
  return ams;
121
127
  }
122
128
 
129
+ /**
130
+ * Read the persisted-and-rehydrated `adjustRanThisSession` flag for a
131
+ * group. Engine consults this on first AMS access so a reactivated group
132
+ * doesn't re-run `runAdjust` on its first turn back online.
133
+ *
134
+ * @param {string|null|undefined} groupId
135
+ * @returns {boolean}
136
+ */
137
+ adjustRanThisSession(groupId) {
138
+ const key = groupId || DEFAULT_GROUP_KEY;
139
+ return this._cache.get(key)?.adjustRanThisSession === true;
140
+ }
141
+
142
+ /**
143
+ * Update the cached `adjustRanThisSession` flag (does not persist on its
144
+ * own — call `persist()` to flush). Engine flips this true after
145
+ * `runAdjust` actually ran.
146
+ *
147
+ * @param {string|null|undefined} groupId
148
+ * @param {boolean} value
149
+ */
150
+ setAdjustRanThisSession(groupId, value) {
151
+ const key = groupId || DEFAULT_GROUP_KEY;
152
+ const entry = this._cache.get(key);
153
+ if (entry) entry.adjustRanThisSession = Boolean(value);
154
+ }
155
+
123
156
  /**
124
157
  * Mark a group's AMS as dirty so the next persist() actually writes.
125
158
  * The engine calls this after `runAdjust` mutates membership.
@@ -134,6 +167,10 @@ export class AmsRegistry {
134
167
  * Persist a single group's AMS to disk. No-op when the cached entry
135
168
  * is missing or hasn't been marked dirty.
136
169
  *
170
+ * `opts.adjustRanThisSession`, when supplied, also updates the cached
171
+ * entry so subsequent `adjustRanThisSession()` reads see the latest flag
172
+ * without a round-trip through disk.
173
+ *
137
174
  * @param {string|null|undefined} groupId
138
175
  * @param {{ force?: boolean, adjustRanThisSession?: boolean }} [opts]
139
176
  * @returns {boolean} true if the file was written
@@ -144,13 +181,17 @@ export class AmsRegistry {
144
181
  if (!entry) return false;
145
182
  if (!opts.force && !this._dirty.has(key)) return false;
146
183
 
184
+ if (typeof opts.adjustRanThisSession === 'boolean') {
185
+ entry.adjustRanThisSession = opts.adjustRanThisSession;
186
+ }
187
+
147
188
  const path = this.amsPath(key);
148
189
  const payload = {
149
190
  version: AMS_FILE_VERSION,
150
191
  ownVpId: entry.ownVpId,
151
192
  onDemandIds: entry.ams.onDemandIds(),
152
193
  recentIds: entry.ams.recentIds(),
153
- adjustRanThisSession: Boolean(opts.adjustRanThisSession),
194
+ adjustRanThisSession: Boolean(entry.adjustRanThisSession),
154
195
  savedAt: new Date().toISOString(),
155
196
  };
156
197
 
@@ -182,15 +223,16 @@ export class AmsRegistry {
182
223
 
183
224
  /**
184
225
  * Best-effort hydrate: read ams.json, re-resolve segment ids via the
185
- * SegmentIndex (skipping ids that no longer exist), populate AMS.
226
+ * SegmentIndex (skipping ids that no longer exist), populate AMS, and
227
+ * restore the persisted `adjustRanThisSession` flag onto the cache entry.
186
228
  * Silent on every error — a corrupt or missing file is the cold-start
187
229
  * case, indistinguishable from "first use of this group".
188
230
  *
189
231
  * @private
190
232
  * @param {string} key
191
- * @param {ActiveMemorySet} ams
233
+ * @param {AmsCacheEntry} entry
192
234
  */
193
- _hydrate(key, ams) {
235
+ _hydrate(key, entry) {
194
236
  const path = this.amsPath(key);
195
237
  if (!existsSync(path)) return;
196
238
  let payload;
@@ -198,6 +240,10 @@ export class AmsRegistry {
198
240
  catch { return; }
199
241
  if (!payload || typeof payload !== 'object') return;
200
242
 
243
+ if (payload.adjustRanThisSession === true) {
244
+ entry.adjustRanThisSession = true;
245
+ }
246
+
201
247
  if (!this.memoryIndex) return;
202
248
 
203
249
  const onDemandIds = Array.isArray(payload.onDemandIds) ? payload.onDemandIds : [];
@@ -210,12 +256,12 @@ export class AmsRegistry {
210
256
  if (seg) onDemandSegs.push(seg);
211
257
  } catch { /* skip unresolvable */ }
212
258
  }
213
- if (onDemandSegs.length > 0) ams.setOnDemand(onDemandSegs);
259
+ if (onDemandSegs.length > 0) entry.ams.setOnDemand(onDemandSegs);
214
260
 
215
261
  for (const id of recentIds) {
216
262
  try {
217
263
  const seg = this.memoryIndex.get(id);
218
- if (seg) ams.touchRecent(seg);
264
+ if (seg) entry.ams.touchRecent(seg);
219
265
  } catch { /* skip */ }
220
266
  }
221
267
  }
package/unify/session.js CHANGED
@@ -47,9 +47,8 @@ import { createV2DreamScheduler } from './dream-v2/session-wiring.js';
47
47
  import { openSegmentIndex } from './memory/index-db.js';
48
48
  import { syncAll as syncSegmentIndex } from './memory/segment-sync.js';
49
49
  import { openAmsRegistry } from './memory/ams-registry.js';
50
- import { migrateR6toV2 } from './memory/migrate-r6-to-v2.js';
51
50
  import { join } from 'path';
52
- import { existsSync as existsSyncSafe, readFileSync as readFileSyncSafe, writeFileSync as writeFileSyncSafe } from 'fs';
51
+ import { existsSync as existsSyncSafe, readFileSync as readFileSyncSafe } from 'fs';
53
52
 
54
53
  /**
55
54
  * @typedef {Object} SessionOptions
@@ -146,53 +145,10 @@ export async function loadSession(options = {}) {
146
145
  }
147
146
  } catch { /* never let this warn path block session load */ }
148
147
 
149
- // ─── 2.2 Auto-migrate R6 → v2 on first boot with memoryV2=on ──
150
- // If `memoryV2` is on AND a R6-shaped tree is on disk AND we
151
- // haven't already migrated, run the one-shot migration. The
152
- // migration is idempotent and concatenate-don't-synthesise, so
153
- // the worst case on re-run is "no R6 dirs found, exit clean".
154
- // Failure here MUST NOT block session boot — we log + continue;
155
- // dream will gradually backfill v2 from group diffs.
156
- try {
157
- if (config?.memoryV2 === true && !config?._readOnly) {
158
- const memoryRoot = join(yeaftDir, 'memory');
159
- const stateFile = join(yeaftDir, '.memory-v2-migration.json');
160
- let alreadyMigrated = false;
161
- if (existsSyncSafe(stateFile)) {
162
- try {
163
- const state = JSON.parse(readFileSyncSafe(stateFile, 'utf8') || '{}');
164
- alreadyMigrated = Boolean(state && state.completedAt);
165
- } catch { /* malformed state → re-run; migration is idempotent */ }
166
- }
167
- const hasR6 = existsSyncSafe(join(memoryRoot, 'groups'))
168
- || existsSyncSafe(join(memoryRoot, 'features'));
169
- if (!alreadyMigrated && hasR6) {
170
- console.log('[Yeaft] memoryV2 on + R6 layout detected — running one-shot migration…');
171
- const result = await migrateR6toV2({ root: memoryRoot, apply: true });
172
- try {
173
- writeFileSyncSafe(stateFile, JSON.stringify({
174
- completedAt: new Date().toISOString(),
175
- migratedScopes: result.migratedScopes,
176
- skippedScopes: result.skippedScopes,
177
- errors: result.errors,
178
- backedUpTo: result.backedUpTo,
179
- }, null, 2));
180
- } catch { /* state file write is best-effort */ }
181
- console.log(`[Yeaft] memory v2 migration done — ${result.migratedScopes} scopes migrated, backup at ${result.backedUpTo}`);
182
- } else if (!alreadyMigrated && !hasR6) {
183
- // Fresh user, no R6 to migrate. Mark as done so we don't keep checking.
184
- try {
185
- writeFileSyncSafe(stateFile, JSON.stringify({
186
- completedAt: new Date().toISOString(),
187
- migratedScopes: 0,
188
- note: 'no R6 layout present — fresh v2',
189
- }, null, 2));
190
- } catch { /* best-effort */ }
191
- }
192
- }
193
- } catch (err) {
194
- console.warn(`[Yeaft] memory v2 migration skipped due to error: ${err?.message || err}`);
195
- }
148
+ // ─── 2.2 R6 → v2 auto-migration retired ───────────────
149
+ // The R6 shard layout is gone memory writes go through
150
+ // dream-v2 directly. Existing users have already migrated
151
+ // (state file in ~/.yeaft/.memory-v2-migration.json).
196
152
 
197
153
  // ─── 2a. Permission pre-check ─────────────────────────
198
154
  // If the data dir is not writable, mark session as read-only.
@@ -4,14 +4,16 @@
4
4
  * Runs after each query loop completes:
5
5
  * 1. Persist messages to conversation/messages/
6
6
  * 2. Consolidate check (compact + extract) — only when budget exceeded
7
- * 3. Dream gate check (background)
8
- * 4. Increment dream query counter
7
+ *
8
+ * Dream V2 owns all background memory maintenance (scope summaries +
9
+ * memory writes via dream-v2/session-wiring.js → createV2DreamScheduler);
10
+ * the legacy `memory/dream.js` gate that used to fire here was retired
11
+ * alongside recall-r6.
9
12
  *
10
13
  * Reference: yeaft-unify-core-systems.md §4.4
11
14
  */
12
15
 
13
16
  import { shouldConsolidate, consolidate } from './memory/consolidate.js';
14
- import { checkDreamGate, incrementQueryCount, dream } from './memory/dream.js';
15
17
  import { isPermissionError } from './init.js';
16
18
 
17
19
  /** Track whether we've already warned about permission issues in stop hooks. */
@@ -59,7 +61,6 @@ export async function runStopHooks(context) {
59
61
  const result = {
60
62
  messagesPersisted: 0,
61
63
  consolidated: false,
62
- dreamTriggered: false,
63
64
  errors: [],
64
65
  };
65
66
 
@@ -164,45 +165,9 @@ export async function runStopHooks(context) {
164
165
  }
165
166
  }
166
167
 
167
- // 3. Increment dream query counter
168
- try {
169
- if (yeaftDir) {
170
- incrementQueryCount(yeaftDir);
171
- }
172
- } catch (err) {
173
- if (isPermissionError(err)) {
174
- // Silent — already warned about permission issues
175
- } else {
176
- result.errors.push(`Dream counter failed: ${err.message}`);
177
- }
178
- }
179
-
180
- // 4. Dream gate check (fire-and-forget, background)
181
- try {
182
- if (yeaftDir && memoryStore && adapter) {
183
- const gate = checkDreamGate(yeaftDir);
184
- if (gate.shouldDream) {
185
- result.dreamTriggered = true;
186
- // Fire and forget — dream runs in background
187
- dream({
188
- yeaftDir,
189
- memoryStore,
190
- conversationStore,
191
- adapter,
192
- config,
193
- }).catch(err => {
194
- trace?.logEvent({
195
- eventType: 'dream_error',
196
- eventData: { error: err.message },
197
- });
198
- });
199
- }
200
- }
201
- } catch (err) {
202
- if (!isPermissionError(err)) {
203
- result.errors.push(`Dream gate check failed: ${err.message}`);
204
- }
205
- }
168
+ // 3. Dream V2 owns background scope-memory maintenance via the session
169
+ // dream scheduler (createV2DreamScheduler). No legacy dream gate is
170
+ // invoked here; the scheduler decides when to run on its own cadence.
206
171
 
207
172
  return result;
208
173
  }
@@ -211,6 +176,5 @@ export async function runStopHooks(context) {
211
176
  * @typedef {Object} StopHookResult
212
177
  * @property {number} messagesPersisted — how many messages were persisted
213
178
  * @property {boolean} consolidated — whether consolidation ran
214
- * @property {boolean} dreamTriggered — whether dream was triggered
215
179
  * @property {string[]} errors — any non-fatal errors
216
180
  */
@@ -1,272 +0,0 @@
1
- /**
2
- * dream-prompt.js — Dream prompt templates for each phase
3
- *
4
- * Dream has 5 phases:
5
- * Phase 1: Orient — assess current memory state
6
- * Phase 2: Gather — collect recent context
7
- * Phase 3: Merge — combine duplicates, update outdated
8
- * Phase 4: Prune — remove stale/low-value entries
9
- * Phase 5: Promote — extract patterns, update profile
10
- *
11
- * Reference: yeaft-unify-core-systems.md §3.3
12
- */
13
-
14
- /**
15
- * Build the Orient phase prompt (Phase 1).
16
- * The LLM assesses the current memory state and identifies issues.
17
- *
18
- * @param {{ memorySummary: string, profileContent: string, entryCount: number }} context
19
- * @returns {string}
20
- */
21
- export function buildOrientPrompt({ memorySummary, profileContent, entryCount }) {
22
- return `You are in Dream Mode — Phase 1: Orient.
23
-
24
- Your task is to assess the current state of the memory store and identify what needs attention.
25
-
26
- ## Current Memory State
27
-
28
- ${memorySummary}
29
-
30
- ## MEMORY.md (User Profile)
31
-
32
- ${profileContent || '(empty)'}
33
-
34
- ## Assessment Instructions
35
-
36
- Review the memory state and provide:
37
- 1. **Redundancies**: Are there entries that overlap or say the same thing?
38
- 2. **Outdated info**: Are there entries that might be stale or no longer relevant?
39
- 3. **Gaps**: Is there important context missing from MEMORY.md?
40
- 4. **Quality**: Are entries well-categorized (kind, scope, tags)?
41
-
42
- Return your assessment as JSON:
43
- {
44
- "redundantGroups": [["entry-a", "entry-b"]],
45
- "potentiallyStale": ["entry-name-1"],
46
- "profileGaps": ["missing X context"],
47
- "qualityIssues": ["entry-y has wrong kind"],
48
- "overallHealth": "good" | "needs-attention" | "poor",
49
- "suggestedActions": ["merge entries about X", "prune stale context entries"]
50
- }
51
-
52
- Return ONLY valid JSON, no other text.`;
53
- }
54
-
55
- /**
56
- * Build the Gather phase prompt (Phase 2).
57
- * Collects recent compact summaries and completed task summaries.
58
- *
59
- * @param {{ recentCompact: string, completedTasks: object[], orientResult: object }} context
60
- * @returns {string}
61
- */
62
- export function buildGatherPrompt({ recentCompact, completedTasks, orientResult }) {
63
- const taskSummaries = completedTasks.length > 0
64
- ? completedTasks.map(t => `- [${t.id}] ${t.description}: ${t.summary || '(no summary)'}`).join('\n')
65
- : '(no recently completed tasks)';
66
-
67
- return `You are in Dream Mode — Phase 2: Gather.
68
-
69
- Your task is to identify what new information should be incorporated into long-term memory.
70
-
71
- ## Recent Conversation Summary (compact.md)
72
-
73
- ${recentCompact || '(no recent summaries)'}
74
-
75
- ## Recently Completed Tasks
76
-
77
- ${taskSummaries}
78
-
79
- ## Orient Assessment
80
-
81
- ${JSON.stringify(orientResult, null, 2)}
82
-
83
- ## Instructions
84
-
85
- From the recent conversations and tasks, identify:
86
- 1. **New facts** worth remembering (project structure, tech decisions)
87
- 2. **New preferences** expressed by the user
88
- 3. **New skills/lessons** learned during tasks
89
- 4. **Context updates** (project progress, status changes)
90
-
91
- Return as JSON:
92
- {
93
- "newEntries": [
94
- { "name": "slug-name", "kind": "fact|preference|skill|lesson|context|relation", "scope": "path", "tags": ["tag1", "tag2"], "importance": "high|normal|low", "content": "description" }
95
- ],
96
- "updatesToExisting": [
97
- { "entryName": "existing-slug", "updates": { "content": "updated text", "tags": ["new-tag"] } }
98
- ]
99
- }
100
-
101
- Return ONLY valid JSON, no other text.`;
102
- }
103
-
104
- /**
105
- * Build the Merge phase prompt (Phase 3).
106
- *
107
- * @param {{ duplicateGroups: object[][], gatherResult: object }} context
108
- * @returns {string}
109
- */
110
- export function buildMergePrompt({ duplicateGroups, gatherResult }) {
111
- const groupDescriptions = duplicateGroups.map((group, i) => {
112
- const entries = group.map(e =>
113
- ` - [${e.name}] kind=${e.kind}, scope=${e.scope}, tags=[${(e.tags || []).join(', ')}]\n ${(e.content || '').slice(0, 200)}`
114
- ).join('\n');
115
- return `Group ${i + 1}:\n${entries}`;
116
- }).join('\n\n');
117
-
118
- return `You are in Dream Mode — Phase 3: Merge.
119
-
120
- Your task is to merge duplicate/overlapping entries into single, richer entries.
121
-
122
- ## Potentially Duplicate Groups
123
-
124
- ${groupDescriptions || '(no duplicates detected)'}
125
-
126
- ## New Entries from Gather Phase
127
-
128
- ${JSON.stringify(gatherResult?.newEntries || [], null, 2)}
129
-
130
- ## Instructions
131
-
132
- For each duplicate group:
133
- 1. Decide if they should be merged (combine info) or kept separate (different enough)
134
- 2. For merges, create a single entry that preserves all important info from both
135
- 3. List which old entries should be deleted after merge
136
-
137
- Also process the new entries from Gather — check if any overlap with existing entries.
138
-
139
- Return as JSON:
140
- {
141
- "merges": [
142
- {
143
- "merged": { "name": "new-slug", "kind": "...", "scope": "...", "tags": [], "importance": "...", "content": "..." },
144
- "deleteOriginals": ["old-entry-1", "old-entry-2"]
145
- }
146
- ],
147
- "newEntries": [
148
- { "name": "...", "kind": "...", "scope": "...", "tags": [], "importance": "...", "content": "..." }
149
- ],
150
- "updates": [
151
- { "entryName": "existing-slug", "updates": { "content": "updated text" } }
152
- ]
153
- }
154
-
155
- Return ONLY valid JSON, no other text.`;
156
- }
157
-
158
- /**
159
- * Build the Prune phase prompt (Phase 4).
160
- *
161
- * @param {{ staleEntries: object[], entryCount: number, maxEntries: number }} context
162
- * @returns {string}
163
- */
164
- export function buildPrunePrompt({ staleEntries, entryCount, maxEntries }) {
165
- const staleDescriptions = staleEntries.map(e =>
166
- `- [${e.name}] kind=${e.kind}, scope=${e.scope}, freq=${e.frequency || 1}, days_since_update=${e._daysSinceUpdate}\n ${(e.content || '').slice(0, 150)}`
167
- ).join('\n');
168
-
169
- return `You are in Dream Mode — Phase 4: Prune.
170
-
171
- Your task is to remove stale, low-value, or redundant entries.
172
-
173
- ## Potentially Stale Entries (${staleEntries.length} found)
174
-
175
- ${staleDescriptions || '(none detected)'}
176
-
177
- ## Capacity
178
-
179
- Current entries: ${entryCount}
180
- Maximum allowed: ${maxEntries}
181
- ${entryCount > maxEntries ? `⚠️ OVER CAPACITY by ${entryCount - maxEntries} entries — must prune aggressively` : 'Within capacity'}
182
-
183
- ## Prune Guidelines
184
-
185
- Delete entries that are:
186
- - **Outdated context**: Project status from weeks ago
187
- - **Never recalled**: frequency=1 and old — nobody needs it
188
- - **Too vague**: "user mentioned something about X" without useful detail
189
- - **Redundant with profile**: If MEMORY.md already captures it
190
- - **Re-derivable**: Info that can be obtained by running a command (e.g., "Node version is 20")
191
-
192
- KEEP entries that are:
193
- - High importance or high frequency
194
- - Recent preferences or lessons
195
- - Facts about project structure (hard to re-discover)
196
-
197
- Return as JSON:
198
- {
199
- "toDelete": ["entry-name-1", "entry-name-2"],
200
- "reasoning": {
201
- "entry-name-1": "outdated context from 45 days ago",
202
- "entry-name-2": "never recalled, too vague"
203
- }
204
- }
205
-
206
- Return ONLY valid JSON, no other text.`;
207
- }
208
-
209
- /**
210
- * Build the Promote phase prompt (Phase 5).
211
- *
212
- * @param {{ entries: object[], profileContent: string, scopesSummary: string }} context
213
- * @returns {string}
214
- */
215
- export function buildPromotePrompt({ entries, profileContent, scopesSummary }) {
216
- // Find entries that might form patterns
217
- const highFreq = entries
218
- .filter(e => (e.frequency || 1) >= 3)
219
- .map(e => `- [${e.name}] kind=${e.kind}, freq=${e.frequency}, scope=${e.scope}: ${(e.content || '').slice(0, 150)}`)
220
- .join('\n');
221
-
222
- const lessons = entries
223
- .filter(e => e.kind === 'lesson')
224
- .map(e => `- [${e.name}] scope=${e.scope}: ${(e.content || '').slice(0, 150)}`)
225
- .join('\n');
226
-
227
- return `You are in Dream Mode — Phase 5: Promote.
228
-
229
- Your task is to identify patterns and update the user profile.
230
-
231
- ## High-Frequency Entries (recalled ≥3 times)
232
-
233
- ${highFreq || '(none)'}
234
-
235
- ## All Lessons
236
-
237
- ${lessons || '(none)'}
238
-
239
- ## Current MEMORY.md Profile
240
-
241
- ${profileContent || '(empty)'}
242
-
243
- ## Scopes
244
-
245
- ${scopesSummary}
246
-
247
- ## Instructions
248
-
249
- 1. **Pattern promotion**: If multiple entries share a pattern, create a higher-level insight
250
- - Example: 3 entries about "user corrects indentation" → 1 preference: "default to 2-space indent"
251
- 2. **Profile update**: Update MEMORY.md sections based on accumulated knowledge
252
- - Keep MEMORY.md under 200 lines
253
- - Sections: Facts, Preferences, Project Context, Skills, Lessons
254
- 3. **Scope promotion**: If a lesson applies across projects, promote scope to parent or global
255
-
256
- Return as JSON:
257
- {
258
- "profileUpdates": {
259
- "Facts": ["- New fact line 1"],
260
- "Preferences": ["- New preference line"],
261
- "Project Context": [],
262
- "Skills": [],
263
- "Lessons": []
264
- },
265
- "promotedEntries": [
266
- { "name": "...", "kind": "...", "scope": "global", "tags": [], "importance": "high", "content": "..." }
267
- ],
268
- "entriesToDelete": ["entry-that-was-promoted-to-profile"]
269
- }
270
-
271
- Return ONLY valid JSON, no other text.`;
272
- }