@yeaft/webchat-agent 0.1.666 → 0.1.669

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.
@@ -35,7 +35,7 @@ import {
35
35
  import { sendToServer, flushMessageBuffer } from './buffer.js';
36
36
  import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
37
37
  import { loadMcpServers, updateMcpConfig } from '../mcp.js';
38
- import { getLlmConfig, updateLlmConfig, getUnifySettings, updateUnifySettings } from '../unify/config-api.js';
38
+ import { getLlmConfig, updateLlmConfig, getUnifySettings, updateUnifySettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../unify/config-api.js';
39
39
  import { handleUnifyChat, handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyFeatureMessage, handleUnifyFetchSummaryHistory, handleUnifyFeatureCrud, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyArchiveGroup, handleUnifyDeleteGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger } from '../unify/web-bridge.js';
40
40
 
41
41
  export async function handleMessage(msg) {
@@ -355,6 +355,29 @@ export async function handleMessage(msg) {
355
355
  break;
356
356
  }
357
357
 
358
+ // Search settings (web-search backend + Tavily key) — read/write the
359
+ // `search` section of config.json. `get_tavily_usage` hits Tavily's
360
+ // /usage endpoint with the saved key and is fired from the UI only
361
+ // when the Search tab opens or the user clicks "Refresh" (no polling
362
+ // — the user explicitly asked for live read on open).
363
+ case 'get_search_settings': {
364
+ const settings = getSearchSettings(ctx.CONFIG?.yeaftDir);
365
+ sendToServer({ type: 'search_settings', ...settings });
366
+ break;
367
+ }
368
+
369
+ case 'update_search_settings': {
370
+ const result = updateSearchSettings(msg.settings || msg.config || {}, ctx.CONFIG?.yeaftDir);
371
+ sendToServer({ type: 'search_settings_updated', ...result });
372
+ break;
373
+ }
374
+
375
+ case 'get_tavily_usage': {
376
+ const usage = await fetchTavilyUsage(ctx.CONFIG?.yeaftDir);
377
+ sendToServer({ type: 'tavily_usage', ...usage });
378
+ break;
379
+ }
380
+
358
381
  // Unify — independent chat via Engine
359
382
  case 'unify_chat':
360
383
  await handleUnifyChat(msg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.666",
3
+ "version": "0.1.669",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -10,7 +10,8 @@
10
10
  },
11
11
  "scripts": {
12
12
  "start": "node index.js",
13
- "dev": "nodemon index.js"
13
+ "dev": "nodemon index.js",
14
+ "postinstall": "node scripts/check-pty.js"
14
15
  },
15
16
  "engines": {
16
17
  "node": ">=22.5.0"
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * agent/scripts/check-pty.js — postinstall sanity check for node-pty.
4
+ *
5
+ * Why this exists:
6
+ * `node-pty` is an optionalDependency. npm install swallows install
7
+ * failures silently — when the native module fails to build (e.g.
8
+ * Linux x64, where node-pty's tarball ships no prebuilds and the
9
+ * host g++ is too old to compile C++20), the agent loses its
10
+ * `terminal` capability with zero user-facing signal. The web UI's
11
+ * Terminal tab silently disappears.
12
+ *
13
+ * This postinstall script makes that failure visible: if we're on a
14
+ * platform where node-pty was supposed to install but didn't, print
15
+ * a clear warning with the recovery command. We never fail the
16
+ * install — pty is genuinely optional, and required-feature mode
17
+ * would block users who don't need a Terminal tab.
18
+ *
19
+ * Exit code is always 0 — we only print, never abort.
20
+ */
21
+
22
+ import { existsSync } from 'fs';
23
+ import { createRequire } from 'module';
24
+ import { platform } from 'os';
25
+
26
+ const require = createRequire(import.meta.url);
27
+
28
+ function tryResolveNodePty() {
29
+ try {
30
+ return require.resolve('node-pty');
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function tryLoadBinary() {
37
+ // node-pty's lib/index.js does `require('../build/Release/pty.node')`
38
+ // — if the binary is missing or ABI-incompatible, that throws. We
39
+ // mimic the load eagerly so the warning fires at install time, not
40
+ // at agent startup.
41
+ try {
42
+ require('node-pty');
43
+ return { ok: true };
44
+ } catch (e) {
45
+ return { ok: false, error: e.message };
46
+ }
47
+ }
48
+
49
+ const resolved = tryResolveNodePty();
50
+ if (!resolved) {
51
+ // node-pty was never installed at all — npm skipped it for the
52
+ // current platform/engine combination.
53
+ const plat = platform();
54
+ if (plat === 'linux' || plat === 'darwin' || plat === 'win32') {
55
+ console.warn('');
56
+ console.warn(' ⚠ node-pty was not installed on this host.');
57
+ console.warn(' The agent will run, but the web UI Terminal tab');
58
+ console.warn(' will be hidden (terminal capability missing).');
59
+ if (plat === 'linux') {
60
+ console.warn('');
61
+ console.warn(' Linux fix: install a C++20-capable compiler');
62
+ console.warn(' (g++-10 or newer) and rebuild:');
63
+ console.warn('');
64
+ console.warn(' sudo apt-get install -y g++-10');
65
+ console.warn(' CXX=g++-10 npm install node-pty --include=optional');
66
+ } else {
67
+ console.warn('');
68
+ console.warn(' Reinstall to retry: npm install node-pty');
69
+ }
70
+ console.warn('');
71
+ }
72
+ process.exit(0);
73
+ }
74
+
75
+ const loaded = tryLoadBinary();
76
+ if (!loaded.ok) {
77
+ console.warn('');
78
+ console.warn(' ⚠ node-pty resolved but failed to load native binary.');
79
+ console.warn(` Error: ${loaded.error}`);
80
+ console.warn(' The agent will run without Terminal tab support.');
81
+ console.warn(' Recover: npm rebuild node-pty');
82
+ console.warn('');
83
+ process.exit(0);
84
+ }
85
+
86
+ // Silent on success — clean install output.
87
+ process.exit(0);
@@ -215,3 +215,154 @@ export function updateUnifySettings(update, dir) {
215
215
 
216
216
  return merged;
217
217
  }
218
+
219
+ // ─── Search settings (web-search backend selection + Tavily key) ────
220
+
221
+ /**
222
+ * Valid backend values. `playwright` is reserved for the upcoming
223
+ * playwright-service tool — its UI option is currently disabled, but we
224
+ * accept the literal so a hand-edited config doesn't trip validation.
225
+ * Anything else is rejected on write and normalized to `tavily` on read.
226
+ */
227
+ const VALID_BACKENDS = ['tavily', 'playwright'];
228
+
229
+ function maskKey(key) {
230
+ if (!key || typeof key !== 'string') return null;
231
+ if (key.length <= 10) return '***';
232
+ return `${key.slice(0, 6)}...${key.slice(-4)}`;
233
+ }
234
+
235
+ /**
236
+ * Read the `search` section of config.json. Tavily key is returned in
237
+ * masked form (`tvly-d...j3dgV`) — the raw key never leaves the agent.
238
+ * UI uses `tavilyKeyConfigured` to decide whether the input shows a
239
+ * "(unchanged)" placeholder vs an empty box.
240
+ *
241
+ * @param {string} [dir]
242
+ * @returns {{ backend: string, tavilyKeyConfigured: boolean, tavilyKeyMasked: string|null, disableHtmlFallback: boolean } | { error: string }}
243
+ */
244
+ export function getSearchSettings(dir) {
245
+ const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
246
+ const configPath = join(root, 'config.json');
247
+ const defaults = {
248
+ backend: 'tavily',
249
+ tavilyKeyConfigured: false,
250
+ tavilyKeyMasked: null,
251
+ disableHtmlFallback: false,
252
+ };
253
+ if (!existsSync(configPath)) return defaults;
254
+ try {
255
+ const json = JSON.parse(readFileSync(configPath, 'utf8'));
256
+ const s = (json && typeof json.search === 'object' && json.search) || {};
257
+ const backend = VALID_BACKENDS.includes(s.backend) ? s.backend : 'tavily';
258
+ const key = typeof s.tavilyApiKey === 'string' ? s.tavilyApiKey : '';
259
+ return {
260
+ backend,
261
+ tavilyKeyConfigured: !!key,
262
+ tavilyKeyMasked: key ? maskKey(key) : null,
263
+ disableHtmlFallback: !!s.disableHtmlFallback,
264
+ };
265
+ } catch (e) {
266
+ return { error: `Failed to read config.json: ${e.message}` };
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Update the `search` section of config.json. Update is shallow-merged:
272
+ * any field omitted from `update` keeps its previous value. Pass
273
+ * `tavilyApiKey: ''` explicitly to clear the key; pass `undefined` (or
274
+ * omit) to keep it unchanged — this is what the UI relies on so the
275
+ * "(unchanged)" placeholder doesn't accidentally wipe a saved key when
276
+ * the user only touches the backend radio.
277
+ *
278
+ * @param {{ backend?: string, tavilyApiKey?: string, disableHtmlFallback?: boolean }} update
279
+ * @param {string} [dir]
280
+ * @returns {ReturnType<typeof getSearchSettings>}
281
+ */
282
+ export function updateSearchSettings(update, dir) {
283
+ const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
284
+ const configPath = join(root, 'config.json');
285
+
286
+ if (!update || typeof update !== 'object') {
287
+ return { error: 'update payload required' };
288
+ }
289
+ if (update.backend !== undefined && !VALID_BACKENDS.includes(update.backend)) {
290
+ return { error: `backend must be one of: ${VALID_BACKENDS.join(', ')}` };
291
+ }
292
+ if (update.tavilyApiKey !== undefined && typeof update.tavilyApiKey !== 'string') {
293
+ return { error: 'tavilyApiKey must be a string' };
294
+ }
295
+
296
+ let existing = {};
297
+ if (existsSync(configPath)) {
298
+ try {
299
+ existing = JSON.parse(readFileSync(configPath, 'utf8'));
300
+ } catch {
301
+ existing = {};
302
+ }
303
+ }
304
+ const prev = (existing && typeof existing.search === 'object' && existing.search) || {};
305
+ const merged = { ...prev };
306
+ if (update.backend !== undefined) merged.backend = update.backend;
307
+ if (update.tavilyApiKey !== undefined) merged.tavilyApiKey = update.tavilyApiKey;
308
+ if (update.disableHtmlFallback !== undefined) merged.disableHtmlFallback = !!update.disableHtmlFallback;
309
+ existing.search = merged;
310
+
311
+ try {
312
+ writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
313
+ } catch (e) {
314
+ return { error: `Failed to write config.json: ${e.message}` };
315
+ }
316
+ return getSearchSettings(root);
317
+ }
318
+
319
+ /**
320
+ * Probe Tavily's `/usage` endpoint with the currently-saved key. Returns
321
+ * the plan + usage fields the UI shows, or `{ error }` for any of:
322
+ * - no key configured
323
+ * - HTTP error from Tavily (401 = bad key, etc.)
324
+ * - network failure
325
+ *
326
+ * Called only when the user opens the Search settings tab (the user
327
+ * explicitly asked for "open settings → live read, don't poll"). No
328
+ * caching here — a stale display is more confusing than a fresh probe.
329
+ *
330
+ * @param {string} [dir]
331
+ * @returns {Promise<{ plan: string, used: number, limit: number|null, paygoUsed: number, paygoLimit: number|null } | { error: string }>}
332
+ */
333
+ export async function fetchTavilyUsage(dir) {
334
+ const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
335
+ const configPath = join(root, 'config.json');
336
+ if (!existsSync(configPath)) return { error: 'config.json not found' };
337
+ let key;
338
+ try {
339
+ const json = JSON.parse(readFileSync(configPath, 'utf8'));
340
+ key = json?.search?.tavilyApiKey;
341
+ } catch (e) {
342
+ return { error: `Failed to read config.json: ${e.message}` };
343
+ }
344
+ if (!key) return { error: 'Tavily API key not configured' };
345
+
346
+ try {
347
+ const res = await fetch('https://api.tavily.com/usage', {
348
+ method: 'GET',
349
+ headers: { Authorization: `Bearer ${key}` },
350
+ });
351
+ if (!res.ok) {
352
+ const text = await res.text().catch(() => '');
353
+ return { error: `${res.status} ${res.statusText} ${text.slice(0, 200)}` };
354
+ }
355
+ const data = await res.json();
356
+ const account = data?.account || {};
357
+ return {
358
+ plan: account.current_plan || 'unknown',
359
+ used: Number(account.plan_usage) || 0,
360
+ limit: account.plan_limit ?? null,
361
+ paygoUsed: Number(account.paygo_usage) || 0,
362
+ paygoLimit: account.paygo_limit ?? null,
363
+ };
364
+ } catch (e) {
365
+ return { error: e.message || String(e) };
366
+ }
367
+ }
368
+
package/unify/config.js CHANGED
@@ -217,7 +217,7 @@ function loadLegacyConfig(dir, overrides) {
217
217
  maxContinueTurns: overrides.maxContinueTurns ?? fileConfig.maxContinueTurns ?? DEFAULTS.maxContinueTurns,
218
218
  // task-318: legacy path never had the `unify` section — defaults.
219
219
  unify: normaliseUnifySection(null),
220
- // DESIGN-v2 feature flag. Default true (PR-E flipped). Override wins.
220
+ // H2-AMS feature flag. Default true. Override wins.
221
221
  memoryV2: overrides.memoryV2 !== undefined ? !!overrides.memoryV2 : true,
222
222
  providers: null,
223
223
  primaryModel: null,
@@ -315,7 +315,7 @@ export function loadConfig(overrides = {}) {
315
315
  // don't pollute the flat config namespace used by chat/crew code.
316
316
  unify: normaliseUnifySection(jsonConfig.unify),
317
317
 
318
- // DESIGN-v2 feature flag. When true the session opens the FTS5
318
+ // H2-AMS feature flag. When true the session opens the FTS5
319
319
  // SegmentIndex (used by groups/pre-flow.js → memory/preflow.js
320
320
  // for pre-turn recall) and wires the v2 dream pipeline
321
321
  // (dream-v2/runner.js). When false both are skipped — no recall,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dream-v2/apply.js — DESIGN-v2 §16 + §17.2.
2
+ * dream-v2/apply.js.
3
3
  *
4
4
  * Execute one merged-target action: either UPDATE an existing scope's
5
5
  * memory.md/summary.md, or CREATE a new one (currently topic-only).
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dream-v2/limits.js — DESIGN-v2 §18 constants.
2
+ * dream-v2/limits.js constants.
3
3
  *
4
4
  * Centralised so tests and runtime share one source of truth. Exposed
5
5
  * as both named exports and a `DEFAULT_LIMITS` object for `runDream()`
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dream-v2/merge.js — DESIGN-v2 §15.
2
+ * dream-v2/merge.js.
3
3
  *
4
4
  * Pure-code stage between Triage and Apply.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dream-v2/runner.js — DESIGN-v2 §13.
2
+ * dream-v2/runner.js.
3
3
  *
4
4
  * Orchestrates one full dream pass:
5
5
  *
@@ -29,7 +29,7 @@
29
29
  *
30
30
  * onProgress emits dream_progress events that the web bridge forwards
31
31
  * as `unify_output` messages so the debug panel can render live state
32
- * (DESIGN-v2 §19.4). All events flow through the same channel; no new
32
+ * . All events flow through the same channel; no new
33
33
  * WebSocket message type is introduced.
34
34
  *
35
35
  * Everything that touches a shell of the system (LLM, message store,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dream-v2/schedule.js — DESIGN-v2 §10.2.
2
+ * dream-v2/schedule.js.
3
3
  *
4
4
  * Two trigger paths:
5
5
  *
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * The scheduler is a thin wrapper around `runDream()` that prevents
12
12
  * concurrent passes (a second tick while the previous is still running
13
- * is dropped, not queued — DESIGN-v2 §10.1: "slow is OK, doesn't
13
+ * is dropped, not queued: "slow is OK, doesn't
14
14
  * compete with user latency").
15
15
  */
16
16
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dream-v2/segment.js — DESIGN-v2 §17.
2
+ * dream-v2/segment.js.
3
3
  *
4
4
  * Three independent length-control concerns, kept pure so they can be
5
5
  * unit-tested without touching disk or any LLM:
@@ -1,13 +1,12 @@
1
1
  /**
2
- * dream-v2/session-wiring.js — DESIGN-v2 §13 wire-up for session.js.
2
+ * dream-v2/session-wiring.js wire-up for session.js.
3
3
  *
4
4
  * Bridges the framework-agnostic `runDream` orchestrator (dream-v2/runner.js)
5
5
  * to the live yeaft session: groups store, conversation log, LLM adapter,
6
6
  * and the engine's progress event sink.
7
7
  *
8
8
  * The dream pipeline only activates when `config.memoryV2 === true`. When
9
- * the flag is off, this module is a no-op — the legacy R6 dream-scheduler
10
- * keeps running as before (session.js still wires it).
9
+ * the flag is off, this module is a no-op.
11
10
  */
12
11
 
13
12
  import { join } from 'path';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dream-v2/snapshot.js — DESIGN-v2 §16.3 + §10.
2
+ * dream-v2/snapshot.js.
3
3
  *
4
4
  * Pre-Apply backup of memory.md + summary.md to
5
5
  * `~/.yeaft/memory/.dream-bak/<ts>/<scope-path>/`. The runner takes a
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dream-v2/state.js — DESIGN-v2 §11.
2
+ * dream-v2/state.js.
3
3
  *
4
4
  * Two pieces of state, tracked separately:
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dream-v2/triage.js — DESIGN-v2 §14.
2
+ * dream-v2/triage.js.
3
3
  *
4
4
  * Decide, for one group's diff, which scopes should be touched by Apply.
5
5
  * The decision is two-staged on purpose:
package/unify/engine.js CHANGED
@@ -557,8 +557,8 @@ export class Engine {
557
557
  * @param {{ profile?: string, entries?: object[] }} [memory]
558
558
  * @param {string} [compactSummary]
559
559
  * @param {string} [prompt] — user prompt (for skill relevance matching)
560
- * @param {string} [memoryInjection] — task-287: prebuilt memory block
561
- * @param {string} [userProfile] — user profile from user-memory shard store
560
+ * @param {string} [memoryInjection] — prebuilt memory block from preflow
561
+ * @param {string} [userProfile] — user profile string
562
562
  * @param {object} [vpPersona]
563
563
  * @param {{user?:string, group?:string, vp?:string}} [summaries]
564
564
  * @returns {string}
@@ -936,12 +936,10 @@ export class Engine {
936
936
  // memory/preflow.js) — per-turn scoped recall
937
937
  // (b) AMS snapshot (resident summaries + onDemand FTS hits) appended
938
938
  // below
939
- // The legacy entries-aggregate `buildMemoryInjection` (index.md / by-project /
940
- // by-topic / timeline) was retired in the H2-AMS rip.
941
939
  let memoryInjection = '';
942
940
  let recallEntryCount = 0;
943
941
 
944
- // R6 recall: append shard-based recall results to memory injection
942
+ // FTS5 recall: append per-turn scoped hits to memory injection
945
943
  const recallResult = await this.#recallMemory(prompt, {
946
944
  groupId,
947
945
  vpId: vpPersona && typeof vpPersona === 'object' && typeof vpPersona.vpId === 'string'
@@ -1,134 +1,11 @@
1
1
  /**
2
2
  * eval/cases/memory.js — Memory recall eval cases
3
3
  *
4
- * Tests the memory recall pipeline:
5
- * - Keyword extraction accuracy
6
- * - Scope + tag filtering
7
- * - LLM selection (when >7 candidates)
8
- * - Fingerprint caching
9
- * - Memory injection into system prompt
4
+ * Smoke-level evals for the H2-AMS memory pipeline. Detailed mocks live
5
+ * in unit tests; this file only declares the eval scenarios.
10
6
  */
11
7
 
12
- import {
13
- noError,
14
- containsText,
15
- custom,
16
- } from '../runner.js';
17
-
18
- // ─── Memory Recall Test Helpers ──────────────────────────────
19
-
20
- /**
21
- * Create an engine with pre-loaded memory entries for eval.
22
- * Uses a mock MemoryStore that returns predefined entries.
23
- */
24
- function createMockMemoryStore(entries) {
25
- return {
26
- readProfile: () => 'User is a senior TypeScript developer who prefers functional programming.',
27
- readEntry: (name) => entries.find(e => e.name === name) || null,
28
- readSection: () => '',
29
- listEntries: () => entries,
30
- findByFilter: ({ scope, tags, limit = 15 }) => {
31
- // Simple scoring: scope match + tag overlap
32
- return entries
33
- .map(e => {
34
- let score = 0;
35
- if (scope && e.scope === scope) score += 3;
36
- if (scope && e.scope === 'global') score += 1;
37
- if (tags) {
38
- for (const t of tags) {
39
- if (e.tags && e.tags.includes(t)) score += 1;
40
- }
41
- }
42
- return { ...e, _score: score };
43
- })
44
- .filter(e => e._score > 0)
45
- .sort((a, b) => b._score - a._score)
46
- .slice(0, limit);
47
- },
48
- bumpFrequency: () => {},
49
- search: (keyword) => entries.filter(e =>
50
- e.content.toLowerCase().includes(keyword.toLowerCase()) ||
51
- e.name.toLowerCase().includes(keyword.toLowerCase()),
52
- ),
53
- stats: () => ({ entryCount: entries.length, scopes: [], kinds: {} }),
54
- writeEntry: () => 'test-entry',
55
- writeEntries: () => [],
56
- deleteEntry: () => true,
57
- rebuildScopes: () => {},
58
- addToSection: () => {},
59
- writeProfile: () => {},
60
- clear: () => {},
61
- };
62
- }
63
-
64
- const sampleMemoryEntries = [
65
- {
66
- name: 'typescript-strict-mode',
67
- kind: 'preference',
68
- scope: 'global',
69
- tags: ['typescript', 'config', 'strict'],
70
- importance: 'high',
71
- frequency: 5,
72
- content: 'User always uses TypeScript strict mode with noImplicitAny enabled.',
73
- created_at: '2026-03-01T00:00:00Z',
74
- updated_at: '2026-04-01T00:00:00Z',
75
- },
76
- {
77
- name: 'prefers-vitest',
78
- kind: 'preference',
79
- scope: 'work/claude-web-chat',
80
- tags: ['testing', 'vitest', 'framework'],
81
- importance: 'normal',
82
- frequency: 3,
83
- content: 'User prefers vitest over jest for testing. Uses vitest for all new projects.',
84
- created_at: '2026-03-15T00:00:00Z',
85
- updated_at: '2026-04-01T00:00:00Z',
86
- },
87
- {
88
- name: 'error-handling-pattern',
89
- kind: 'lesson',
90
- scope: 'global',
91
- tags: ['error-handling', 'typescript', 'patterns'],
92
- importance: 'high',
93
- frequency: 4,
94
- content: 'Always use Result<T, E> pattern instead of throwing exceptions. Wrap external API calls in try-catch and return Result.',
95
- created_at: '2026-02-01T00:00:00Z',
96
- updated_at: '2026-04-01T00:00:00Z',
97
- },
98
- {
99
- name: 'project-structure',
100
- kind: 'context',
101
- scope: 'work/claude-web-chat',
102
- tags: ['architecture', 'project', 'monorepo'],
103
- importance: 'normal',
104
- frequency: 2,
105
- content: 'Project uses monorepo with agent/, server/, web/ directories. Agent code is in agent/unify/.',
106
- created_at: '2026-01-01T00:00:00Z',
107
- updated_at: '2026-03-01T00:00:00Z',
108
- },
109
- {
110
- name: 'functional-programming',
111
- kind: 'preference',
112
- scope: 'global',
113
- tags: ['functional', 'programming', 'style'],
114
- importance: 'normal',
115
- frequency: 6,
116
- content: 'User prefers functional programming: pure functions, immutable data, map/filter/reduce over loops.',
117
- created_at: '2026-01-15T00:00:00Z',
118
- updated_at: '2026-04-05T00:00:00Z',
119
- },
120
- {
121
- name: 'api-design-rest',
122
- kind: 'skill',
123
- scope: 'global',
124
- tags: ['api', 'rest', 'design'],
125
- importance: 'normal',
126
- frequency: 1,
127
- content: 'REST API conventions: use plural nouns, HTTP methods for CRUD, 2xx success, 4xx client error, 5xx server error.',
128
- created_at: '2026-02-15T00:00:00Z',
129
- updated_at: '2026-02-15T00:00:00Z',
130
- },
131
- ];
8
+ import { noError, custom } from '../runner.js';
132
9
 
133
10
  // ─── Eval Cases ──────────────────────────────────────────────
134
11
 
@@ -139,12 +16,8 @@ export const memoryCases = [
139
16
  {
140
17
  id: 'memory-profile-injection',
141
18
  suite: 'memory',
142
- description: 'System prompt should include user profile from memory',
19
+ description: 'System prompt should include memory section after preflow',
143
20
  prompt: 'Help me with a coding task',
144
- setupEngine: (engine) => {
145
- // We can't directly inject memoryStore here since Engine uses private fields
146
- // Instead, this eval verifies via the adapter call log that system prompt contains memory
147
- },
148
21
  criteria: [
149
22
  noError,
150
23
  custom('has-response', 'Model produces a response', 5, (result) => ({
@@ -154,29 +27,23 @@ export const memoryCases = [
154
27
  ],
155
28
  },
156
29
 
157
- // ─── Keyword Extraction (unit-level eval) ─────────────
30
+ // ─── Recall Event ──────────────────────────────────────
158
31
 
159
32
  {
160
33
  id: 'memory-keyword-extraction',
161
34
  suite: 'memory',
162
- description: 'Keyword extraction produces relevant keywords',
35
+ description: 'Recall event emitted when preflow has hits',
163
36
  prompt: 'How should I handle TypeScript errors in my Express API?',
164
37
  criteria: [
165
38
  noError,
166
- // This is tested at unit level but verifiable here via recall event
167
- custom('recall-event', 'Recall event emitted (if memory store provided)', 3, (result) => {
168
- // Without a real memory store this won't emit recall, so we check gracefully
39
+ custom('recall-event', 'Recall event emitted (if FTS index seeded)', 3, (result) => {
169
40
  const recallEvent = result.events.find(e => e.type === 'recall');
170
41
  return {
171
- pass: true, // Always passes — it's informational
42
+ pass: true,
172
43
  score: recallEvent ? 1 : 0.5,
173
- reason: recallEvent ? `Recalled ${recallEvent.entryCount} entries` : 'No memory store configured',
44
+ reason: recallEvent ? `Recalled ${recallEvent.entryCount} segments` : 'No FTS hits',
174
45
  };
175
46
  }),
176
47
  ],
177
48
  },
178
49
  ];
179
-
180
- // ─── Exported for direct import in unit tests ────────────────
181
-
182
- export { createMockMemoryStore, sampleMemoryEntries };
@@ -11,7 +11,7 @@
11
11
  * truth is `<scope>/memory.md` + `<scope>/summary.md`. AMS itself
12
12
  * doesn't write to disk — that's Dream's job.
13
13
  *
14
- * Privacy (DESIGN-v2 §2.2): `vp/<other>` scopes are ALWAYS filtered out
14
+ * Privacy: `vp/<other>` scopes are ALWAYS filtered out
15
15
  * for any worker that isn't `<other>`. The owning code passes its own
16
16
  * vpId at construction.
17
17
  */
@@ -5,7 +5,7 @@
5
5
  * segment blocks) and the SQLite segment index. This layer handles
6
6
  * scope <-> file path mapping; the index layer is scope-agnostic.
7
7
  *
8
- * Path conventions (DESIGN-v2 §5):
8
+ * Path conventions:
9
9
  * ~/.yeaft/memory/user/memory.md
10
10
  * ~/.yeaft/memory/vp/<id>/memory.md
11
11
  * ~/.yeaft/memory/group/<id>/memory.md
@@ -1,5 +1,5 @@
1
1
  /**
2
- * memory/store-v2.js — DESIGN-v2.md Part I: per-scope memory.md + summary.md.
2
+ * memory/store-v2.js — per-scope memory.md + summary.md (Layer-A storage).
3
3
  *
4
4
  * One pair of files per scope. No shards, no entries/, no index.md, no
5
5
  * index.json. The five scope kinds — user, vp, group, feature, topic — share
@@ -29,18 +29,15 @@
29
29
  * ACL:
30
30
  * - This module enforces ONE ACL: `vp/<other>` paths are blocked when
31
31
  * `currentVpId` is given and differs from `<other>`. Every other scope
32
- * boundary is ACL-free in v2 (DESIGN-v2 §3.2).
32
+ * boundary is ACL-free.
33
33
  *
34
34
  * What this module deliberately does NOT do:
35
35
  * - No frontmatter parsing. memory.md and summary.md are pure markdown;
36
36
  * the dream-state metadata block lives at the file's tail and is read
37
37
  * by `dream-v2/state.js`, not here.
38
38
  * - No LLM calls, no extraction, no summarisation. Pure I/O.
39
- * - No legacy R6 fallback. The old MemoryStore (memory/store.js) and
40
- * ScopeTree (memory/scope-tree.js) remain in service until PR-E swaps
41
- * callers; this module is additive.
42
39
  *
43
- * Reference: agent/unify/memory/DESIGN-v2.md §2, §5, §9.
40
+ * Reference: agent/unify/memory/DESIGN-H2-AMS.md.
44
41
  */
45
42
 
46
43
  import {
@@ -228,8 +225,8 @@ export async function writeMemory(scope, content, opts = {}) {
228
225
  }
229
226
 
230
227
  /**
231
- * Append to a scope's memory.md. Used by the rare "direct write" path
232
- * (DESIGN-v2 §7.1); main flow is dream-driven rewrites.
228
+ * Append to a scope's memory.md. Used by the rare "direct write" path;
229
+ * main flow is dream-driven rewrites.
233
230
  *
234
231
  * Append is non-atomic with concurrent readers in the strict sense, but a
235
232
  * single appendFile of a small buffer is atomic at the kernel level on POSIX
@@ -316,7 +313,7 @@ export async function ensureScope(scope, opts = {}) {
316
313
 
317
314
  /**
318
315
  * Enumerate all scopes present on disk. Returns Scope shapes that round-trip
319
- * back through `scopeDir`. Used by Triage (DESIGN-v2 §14) to list candidate
316
+ * back through `scopeDir`. Used by Triage to list candidate
320
317
  * scopes for a group's diff.
321
318
  *
322
319
  * Walks shallowly:
package/unify/prompts.js CHANGED
@@ -8,39 +8,13 @@
8
8
  * and used to enrich the system prompt beyond the hardcoded fallbacks.
9
9
  *
10
10
  * Phase 2 additions:
11
- * - Memory section (user profile + recalled entries)
11
+ * - Memory section (recalled segments via H2-AMS pre-flow)
12
12
  * - Compact summary section (conversation history summary)
13
13
  *
14
- * task-287 refactor (tool-on-demand memory):
15
- * - New `memoryInjection` param carries prebuilt "Memory Index + user
16
- * preferences + project header" text (~1.5k tokens). Engine builds this
17
- * via memory/layout.buildMemoryInjection() and passes it every turn.
18
- * - Legacy `memory={profile,entries}` param still supported for callers
19
- * (tests, CLI) that have not migrated.
20
- *
21
- * task-334e additions (R6 §Δ24.5 / §Δ27.3 / §Δ31.4 / §Δ29.3):
22
- * - `taskCtx` param → renders a `## task_ctx` block with:
23
- * * task-memory top-5 bodies with semantic shard prefix `[shard]`
24
- * (no sourceRef — memory_trace opens the trail on demand)
25
- * * `### related tasks` sub-section — `relatedTaskIds` top-3 (sorted
26
- * by updatedAt desc) + per-task top-2 memory; ACL-gated by
27
- * `target.members` ∋ currentVpId (§Δ31.4)
28
- * * `### summary reminder` soft nudge — condition (§Δ27.3):
29
- * non-summary msgs ≥ 3 AND since-lastSummary > 15min AND
30
- * currentVpId == task.initiatorVpId → emits a DYNAMIC hint
31
- * - `userProfile` param → renders a `## user_profile` block. Stub
32
- * implementation: when not passed explicitly, we fall back to reading
33
- * `~/.yeaft/user/profile.json` ({ "content": "…string…" }) if present
34
- * (§Δ29.3 placeholder until 334l wires real user-memory recall).
35
- * - `coreMemory` param → renders a `## core_memory` block with recall
36
- * top-7 memory bodies (no sourceRef) and a trailing meta line pointing
37
- * at `memory_trace` as the way to open the original message.
38
- *
39
- * Hard constraints (task-334e contract):
40
- * - Does NOT modify engine.js turn loop.
41
- * - Does NOT implement memory_trace / open_source_message (task-334f).
42
- * - Does NOT implement task_summary_post (task-334n).
43
- * - Changes limited to prompts.js + templates/.
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.
44
18
  *
45
19
  * Reference: yeaft-unify-system-prompt-budget.md — Static + Dynamic + Context layers
46
20
  */
package/unify/session.js CHANGED
@@ -282,7 +282,7 @@ export async function loadSession(options = {}) {
282
282
  yeaftDir,
283
283
  });
284
284
 
285
- // ─── 9a. Create dream scheduler (DESIGN-v2) ────────────
285
+ // ─── 9a. Create dream scheduler ────────────
286
286
  // The legacy R6 dream-scheduler was retired alongside recall-r6;
287
287
  // dream-v2 is the only active path. The `memoryV2: false` opt-out
288
288
  // no longer leaves a usable system, so we always wire v2 here.
@@ -60,6 +60,14 @@ Guidelines:
60
60
  const signal = ctx?.signal;
61
61
  const errors = [];
62
62
 
63
+ // Backend preference set in UnifySettings → Search tab. When the
64
+ // user picks `playwright` we'd normally call the playwright-service;
65
+ // that service is not yet shipped (next PR), so the preference is
66
+ // recorded for forward-compat and we transparently fall through to
67
+ // Tavily / HTML scrape. Once the service lands we'll insert a
68
+ // tryPlaywright backend here ahead of Tavily.
69
+ // Anything other than 'playwright' is treated as 'tavily'.
70
+
63
71
  // 1. Tavily — default, fast, structured.
64
72
  if (search.tavilyApiKey) {
65
73
  const r = await tryTavily(query, limit, search.tavilyApiKey, signal);