@yeaft/webchat-agent 0.1.661 → 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/unify/session.js CHANGED
@@ -32,18 +32,23 @@ import { Engine } from './engine.js';
32
32
  // SegmentIndex (SQLite FTS5 over memory.md) and passes it to the
33
33
  // Engine. Engine.#recallMemory routes pre-turn recall through
34
34
  // groups/pre-flow.js → memory/preflow.js (the previous per-scope
35
- // file reader recall-v2.js has been deleted). Post-turn AMS
36
- // correction (memory/adjust.js) is implemented but not yet wired —
37
- // requires session-level AMS instance + scope resolution. Tracked
38
- // as a follow-up.
35
+ // file reader recall-v2.js has been deleted).
36
+ //
37
+ // GC.1 follow-up: when memoryIndex is wired we also open an
38
+ // AmsRegistry. The registry caches per-group ActiveMemorySet
39
+ // instances and persists their identity-only state under
40
+ // `~/.yeaft/memory/groups/<gid>/ams.json` so a deactivated group
41
+ // resumes with the same onDemand/recent membership it had on
42
+ // disconnect. Engine.#runQuery uses the registry to populate the
43
+ // AMS each turn and to run `memory/adjust.js` post-turn.
39
44
  import { ensureDefaultGroupIfEmpty } from './groups/group-crud.js';
40
45
  import { seedDefaultVps } from './vp/seed-defaults.js';
41
46
  import { createV2DreamScheduler } from './dream-v2/session-wiring.js';
42
47
  import { openSegmentIndex } from './memory/index-db.js';
43
48
  import { syncAll as syncSegmentIndex } from './memory/segment-sync.js';
44
- import { migrateR6toV2 } from './memory/migrate-r6-to-v2.js';
49
+ import { openAmsRegistry } from './memory/ams-registry.js';
45
50
  import { join } from 'path';
46
- import { existsSync as existsSyncSafe, readFileSync as readFileSyncSafe, writeFileSync as writeFileSyncSafe } from 'fs';
51
+ import { existsSync as existsSyncSafe, readFileSync as readFileSyncSafe } from 'fs';
47
52
 
48
53
  /**
49
54
  * @typedef {Object} SessionOptions
@@ -140,53 +145,10 @@ export async function loadSession(options = {}) {
140
145
  }
141
146
  } catch { /* never let this warn path block session load */ }
142
147
 
143
- // ─── 2.2 Auto-migrate R6 → v2 on first boot with memoryV2=on ──
144
- // If `memoryV2` is on AND a R6-shaped tree is on disk AND we
145
- // haven't already migrated, run the one-shot migration. The
146
- // migration is idempotent and concatenate-don't-synthesise, so
147
- // the worst case on re-run is "no R6 dirs found, exit clean".
148
- // Failure here MUST NOT block session boot — we log + continue;
149
- // dream will gradually backfill v2 from group diffs.
150
- try {
151
- if (config?.memoryV2 === true && !config?._readOnly) {
152
- const memoryRoot = join(yeaftDir, 'memory');
153
- const stateFile = join(yeaftDir, '.memory-v2-migration.json');
154
- let alreadyMigrated = false;
155
- if (existsSyncSafe(stateFile)) {
156
- try {
157
- const state = JSON.parse(readFileSyncSafe(stateFile, 'utf8') || '{}');
158
- alreadyMigrated = Boolean(state && state.completedAt);
159
- } catch { /* malformed state → re-run; migration is idempotent */ }
160
- }
161
- const hasR6 = existsSyncSafe(join(memoryRoot, 'groups'))
162
- || existsSyncSafe(join(memoryRoot, 'features'));
163
- if (!alreadyMigrated && hasR6) {
164
- console.log('[Yeaft] memoryV2 on + R6 layout detected — running one-shot migration…');
165
- const result = await migrateR6toV2({ root: memoryRoot, apply: true });
166
- try {
167
- writeFileSyncSafe(stateFile, JSON.stringify({
168
- completedAt: new Date().toISOString(),
169
- migratedScopes: result.migratedScopes,
170
- skippedScopes: result.skippedScopes,
171
- errors: result.errors,
172
- backedUpTo: result.backedUpTo,
173
- }, null, 2));
174
- } catch { /* state file write is best-effort */ }
175
- console.log(`[Yeaft] memory v2 migration done — ${result.migratedScopes} scopes migrated, backup at ${result.backedUpTo}`);
176
- } else if (!alreadyMigrated && !hasR6) {
177
- // Fresh user, no R6 to migrate. Mark as done so we don't keep checking.
178
- try {
179
- writeFileSyncSafe(stateFile, JSON.stringify({
180
- completedAt: new Date().toISOString(),
181
- migratedScopes: 0,
182
- note: 'no R6 layout present — fresh v2',
183
- }, null, 2));
184
- } catch { /* best-effort */ }
185
- }
186
- }
187
- } catch (err) {
188
- console.warn(`[Yeaft] memory v2 migration skipped due to error: ${err?.message || err}`);
189
- }
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).
190
152
 
191
153
  // ─── 2a. Permission pre-check ─────────────────────────
192
154
  // If the data dir is not writable, mark session as read-only.
@@ -250,6 +212,22 @@ export async function loadSession(options = {}) {
250
212
  }
251
213
  }
252
214
 
215
+ // ─── 5-ams. (GC.1 follow-up) Group-keyed AMS registry ────
216
+ // The registry caches one ActiveMemorySet per groupId and
217
+ // persists their state to disk so a deactivated group can be
218
+ // reactivated with the same onDemand/recent membership it had
219
+ // on disconnect. Without memoryIndex we have nothing to
220
+ // re-hydrate against, so the registry is left null in that case.
221
+ let amsRegistry = null;
222
+ if (memoryIndex && !config._readOnly) {
223
+ try {
224
+ amsRegistry = openAmsRegistry({ yeaftDir, memoryIndex, config });
225
+ } catch (err) {
226
+ console.warn(`[Yeaft] Failed to open AMS registry (adjust disabled): ${err?.message || err}`);
227
+ amsRegistry = null;
228
+ }
229
+ }
230
+
253
231
  // ─── 5a. Initialize feature store ──────────────────────
254
232
  initFeatureStore(yeaftDir, { readOnly: config._readOnly || false });
255
233
 
@@ -315,6 +293,7 @@ export async function loadSession(options = {}) {
315
293
  memoryStore,
316
294
  memoryShardStore,
317
295
  memoryIndex,
296
+ amsRegistry,
318
297
  toolRegistry,
319
298
  skillManager,
320
299
  mcpManager,
@@ -372,6 +351,11 @@ export async function loadSession(options = {}) {
372
351
  } catch {
373
352
  // Best-effort cleanup
374
353
  }
354
+ try {
355
+ if (amsRegistry) amsRegistry.persistAll();
356
+ } catch {
357
+ // Best-effort cleanup
358
+ }
375
359
  }
376
360
 
377
361
  return {
@@ -388,6 +372,7 @@ export async function loadSession(options = {}) {
388
372
  trace,
389
373
  yeaftDir,
390
374
  status,
375
+ amsRegistry,
391
376
  shutdown,
392
377
  // task-325c: user-initiated abort API. Delegates to web-bridge which
393
378
  // owns the single AbortController. Lazy-imported to avoid a hard cycle
@@ -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
- }