@yeaft/webchat-agent 0.1.662 → 0.1.663
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 +1 -1
- package/unify/cli.js +0 -10
- package/unify/engine.js +9 -3
- package/unify/memory/ams-registry.js +57 -11
- package/unify/session.js +5 -49
- package/unify/stop-hooks.js +8 -44
- package/unify/memory/dream-prompt.js +0 -272
- package/unify/memory/dream.js +0 -783
- package/unify/memory/migrate-r6-to-v2.js +0 -462
- package/unify/router/vp-planner.js +0 -341
package/package.json
CHANGED
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);
|
|
@@ -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
|
|
85
|
-
return join(this.yeaftDir, 'memory', 'groups',
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
this.
|
|
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(
|
|
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 {
|
|
233
|
+
* @param {AmsCacheEntry} entry
|
|
192
234
|
*/
|
|
193
|
-
_hydrate(key,
|
|
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
|
|
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
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
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.
|
package/unify/stop-hooks.js
CHANGED
|
@@ -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
|
-
*
|
|
8
|
-
*
|
|
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.
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
}
|