@yeaft/webchat-agent 1.0.75 → 1.0.77

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.
@@ -8,14 +8,24 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
8
8
  import { join } from 'path';
9
9
  import { parseMessage, parseSeqFromId } from './persist.js';
10
10
 
11
+ function parseJsonLine(line) {
12
+ if (!line || !line.trim()) return null;
13
+ try {
14
+ const msg = JSON.parse(line);
15
+ return msg && typeof msg === 'object' ? msg : null;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
11
21
  /**
12
- * Search messages in a directory for a keyword.
22
+ * Search Markdown messages in a directory for a keyword.
13
23
  *
14
24
  * @param {string} dir — messages directory
15
25
  * @param {string} keyword — search term (case-insensitive)
16
26
  * @returns {object[]} — matching messages
17
27
  */
18
- function searchDir(dir, keyword) {
28
+ function searchMarkdownDir(dir, keyword) {
19
29
  if (!existsSync(dir)) return [];
20
30
 
21
31
  const lowerKeyword = keyword.toLowerCase();
@@ -35,6 +45,29 @@ function searchDir(dir, keyword) {
35
45
  return results;
36
46
  }
37
47
 
48
+ function searchSegmentDir(dir, keyword) {
49
+ if (!existsSync(dir)) return [];
50
+ const lowerKeyword = keyword.toLowerCase();
51
+ const files = readdirSync(dir)
52
+ .filter(f => f.endsWith('.jsonl'))
53
+ .sort()
54
+ .reverse();
55
+
56
+ const results = [];
57
+ for (const file of files) {
58
+ const raw = readFileSync(join(dir, file), 'utf8');
59
+ if (!raw.toLowerCase().includes(lowerKeyword)) continue;
60
+ const lines = raw.split('\n');
61
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
62
+ const line = lines[i];
63
+ if (!line || !line.toLowerCase().includes(lowerKeyword)) continue;
64
+ const msg = parseJsonLine(line);
65
+ if (msg) results.push(msg);
66
+ }
67
+ }
68
+ return results;
69
+ }
70
+
38
71
  function compareNewest(a, b) {
39
72
  const sa = parseSeqFromId(a?.id);
40
73
  const sb = parseSeqFromId(b?.id);
@@ -42,7 +75,7 @@ function compareNewest(a, b) {
42
75
  return String(b?.time || '').localeCompare(String(a?.time || ''));
43
76
  }
44
77
 
45
- function sessionConversationMessageDirs(dir) {
78
+ function sessionConversationDirs(dir) {
46
79
  const dirs = [];
47
80
  const seen = new Set();
48
81
  for (const rootName of ['sessions', 'groups']) {
@@ -57,12 +90,9 @@ function sessionConversationMessageDirs(dir) {
57
90
  }
58
91
 
59
92
  const conversationDir = join(sessionDir, 'conversation');
60
- for (const kind of ['messages', 'cold']) {
61
- const messagesDir = join(conversationDir, kind);
62
- if (seen.has(messagesDir)) continue;
63
- seen.add(messagesDir);
64
- dirs.push(messagesDir);
65
- }
93
+ if (seen.has(conversationDir)) continue;
94
+ seen.add(conversationDir);
95
+ dirs.push(conversationDir);
66
96
  }
67
97
  }
68
98
  return dirs;
@@ -79,17 +109,23 @@ function sessionConversationMessageDirs(dir) {
79
109
  export function searchMessages(dir, keyword, limit = 20) {
80
110
  if (!keyword || !keyword.trim()) return [];
81
111
 
82
- const dirs = [
83
- join(dir, 'chat', 'messages'),
84
- join(dir, 'chat', 'cold'),
85
- ...sessionConversationMessageDirs(dir),
112
+ const conversationDirs = [
113
+ join(dir, 'chat'),
114
+ ...sessionConversationDirs(dir),
115
+ ];
116
+
117
+ const markdownDirs = [
118
+ ...conversationDirs.flatMap(d => [join(d, 'messages'), join(d, 'cold')]),
86
119
  // Compatibility for profiles created before chat/session split.
87
120
  join(dir, 'conversation', 'messages'),
88
121
  join(dir, 'conversation', 'cold'),
89
122
  ];
123
+ const segmentDirs = conversationDirs.map(d => join(d, 'segments'));
90
124
 
91
- return dirs
92
- .flatMap(d => searchDir(d, keyword))
125
+ return [
126
+ ...segmentDirs.flatMap(d => searchSegmentDir(d, keyword)),
127
+ ...markdownDirs.flatMap(d => searchMarkdownDir(d, keyword)),
128
+ ]
93
129
  .sort(compareNewest)
94
130
  .slice(0, limit);
95
131
  }
package/yeaft/session.js CHANGED
@@ -45,7 +45,7 @@ import { TaskManager } from './tasks/manager.js';
45
45
  // resumes with the same onDemand/recent membership it had on
46
46
  // disconnect. Engine.#runQuery uses the registry to populate the
47
47
  // AMS each turn and to run `memory/adjust.js` post-turn.
48
- import { ensureDefaultSessionIfEmpty } from './sessions/session-crud.js';
48
+ import { ensureDefaultSessionIfEmpty, yeaftDirForWorkDir } from './sessions/session-crud.js';
49
49
  import { seedDefaultVps } from './vp/seed-defaults.js';
50
50
  import { topUpDefaultVps } from './vp/seed-topup.js';
51
51
  import { archiveLegacyScopes } from './memory/seed-backfill.js';
@@ -71,6 +71,7 @@ const DEFAULT_COMPACT_TRIGGER_RATIO = 0.7;
71
71
  /**
72
72
  * @typedef {Object} SessionOptions
73
73
  * @property {string} [dir] — Yeaft data directory override (default: ~/.yeaft)
74
+ * @property {string} [workDir] — Session workDir; when provided, storage lives under <workDir>/.yeaft while config still comes from dir/defaults
74
75
  * @property {string} [model] — Model override
75
76
  * @property {string} [language] — Language override ('en' | 'zh')
76
77
  * @property {boolean} [debug] — Debug mode override
@@ -136,6 +137,7 @@ function prepareToolStatsDir(yeaftDir) {
136
137
  export async function loadSession(options = {}) {
137
138
  const {
138
139
  dir,
140
+ workDir,
139
141
  model,
140
142
  language,
141
143
  debug,
@@ -146,21 +148,29 @@ export async function loadSession(options = {}) {
146
148
  serverMode = false,
147
149
  } = options;
148
150
 
149
- // ─── 1. Determine yeaftDir + ensure directory structure ──
150
- // Must happen BEFORE loadConfig so that first-run
151
- // generates a default config.json that loadConfig can read.
151
+ // ─── 1. Determine config + store directories ─────────────
152
+ // Must happen BEFORE loadConfig so that first-run generates a
153
+ // default config.json that loadConfig can read. workDir-backed
154
+ // Sessions store conversation/session data under <workDir>/.yeaft,
155
+ // but runtime config still comes from the agent-local configDir.
152
156
  const overrides = { ...configOverrides };
153
157
  if (dir) overrides.dir = dir;
154
158
  if (model) overrides.model = model;
155
159
  if (language) overrides.language = language;
156
160
  if (debug !== undefined) overrides.debug = debug;
157
161
 
158
- const yeaftDir = overrides.dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
159
- const initResult = initYeaftDir(yeaftDir);
160
- overrides.dir = yeaftDir;
161
-
162
- // Log any warnings from directory initialization
163
- for (const w of initResult.warnings) {
162
+ const sessionWorkDir = typeof workDir === 'string' && workDir.trim() ? workDir.trim() : '';
163
+ const configDir = overrides.dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
164
+ const yeaftDir = sessionWorkDir ? yeaftDirForWorkDir(sessionWorkDir) : configDir;
165
+ const configInitResult = initYeaftDir(configDir);
166
+ const storeInitResult = yeaftDir === configDir ? configInitResult : initYeaftDir(yeaftDir);
167
+ overrides.dir = configDir;
168
+
169
+ // Log any warnings from directory initialization.
170
+ const initWarnings = yeaftDir === configDir
171
+ ? configInitResult.warnings
172
+ : [...configInitResult.warnings, ...storeInitResult.warnings];
173
+ for (const w of initWarnings) {
164
174
  console.warn(`[Yeaft] ${w}`);
165
175
  }
166
176
 
@@ -212,7 +222,7 @@ export async function loadSession(options = {}) {
212
222
  // ─── 2a. Permission pre-check ─────────────────────────
213
223
  // If the data dir is not writable, mark session as read-only.
214
224
  // Persistence (conversation, memory, dream) is skipped in this mode.
215
- if (!initResult.writable) {
225
+ if (!storeInitResult.writable) {
216
226
  config._readOnly = true;
217
227
  console.warn(`[Yeaft] ${yeaftDir} is not writable — running in read-only mode`);
218
228
  }
@@ -365,24 +375,23 @@ export async function loadSession(options = {}) {
365
375
  }
366
376
 
367
377
  // ─── 6. Load skills ────────────────────────────────────
368
- // Project tier root for skills + MCP project assets. Per-session workDir
369
- // overlays are loaded by web-bridge once it knows the selected Session meta;
370
- // the base runtime still uses the agent process cwd for global/default status.
371
- const projectTierRoot = process.cwd();
378
+ // User/global runtime assets still come from configDir. workDir, when
379
+ // present, is only a project tier overlay plus the storage root.
380
+ const projectTierRoot = sessionWorkDir || process.cwd();
372
381
 
373
382
  let skillManager;
374
383
  if (skipSkills) {
375
384
  // Pass the literal user-tier dir (matches the normal branch's tier 2)
376
385
  // so any save/remove calls land in the same place users expect. New
377
386
  // `SkillManager` API takes literal scan dirs — no auto-suffix of /skills.
378
- skillManager = new SkillManager(join(yeaftDir, 'skills'));
387
+ skillManager = new SkillManager(join(configDir, 'skills'));
379
388
  // Don't call .load() — empty skill manager
380
389
  } else {
381
- skillManager = createSkillManager(yeaftDir, projectTierRoot);
390
+ skillManager = createSkillManager(configDir, projectTierRoot);
382
391
  }
383
392
 
384
393
  // ─── 7. Connect MCP servers ────────────────────────────
385
- const mcpConfig = loadMCPConfig(yeaftDir, undefined, projectTierRoot);
394
+ const mcpConfig = loadMCPConfig(configDir, undefined, projectTierRoot);
386
395
  const mcpManager = new MCPManager();
387
396
  let mcpStatus = { connected: [], failed: [] };
388
397
 
@@ -53,6 +53,7 @@ import {
53
53
  scanWorkdirSessions,
54
54
  restoreSessionToRegistry,
55
55
  readWorkDirRegistry,
56
+ yeaftDirForWorkDir,
56
57
  } from './sessions/session-crud.js';
57
58
  import { openSession, loadSessionMeta } from './sessions/session-store.js';
58
59
  import { loadSessionConfig, resolveSessionConfig, SessionConfigError } from './sessions/session-config.js';
@@ -414,6 +415,13 @@ function projectRuntimeKey(workDir) {
414
415
  return normalizeSessionWorkDir(workDir) || '__agent_cwd__';
415
416
  }
416
417
 
418
+ function resolveStoreYeaftDirForSession(defaultYeaftDir, { sessionId = null, sessionMeta = null, workDir = '' } = {}) {
419
+ const normalizedWorkDir = normalizeSessionWorkDir(workDir || sessionMeta?.workDir);
420
+ if (normalizedWorkDir) return yeaftDirForWorkDir(normalizedWorkDir);
421
+ if (sessionId) return resolveSessionYeaftDir(defaultYeaftDir, sessionId);
422
+ return defaultYeaftDir;
423
+ }
424
+
417
425
  function createThreadId() {
418
426
  return `thr_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
419
427
  }
@@ -3880,6 +3888,11 @@ async function ensureSessionLoaded(opts = {}) {
3880
3888
  sessionLoadPromise = (async () => {
3881
3889
  const yeaftDir = ctx.CONFIG?.yeaftDir;
3882
3890
  const normalizedWorkDir = normalizeSessionWorkDir(opts?.workDir || opts?.sessionMeta?.workDir);
3891
+ const sessionYeaftDir = resolveStoreYeaftDirForSession(yeaftDir, {
3892
+ sessionId: opts?.sessionId || opts?.sessionMeta?.id || null,
3893
+ sessionMeta: opts?.sessionMeta || null,
3894
+ workDir: normalizedWorkDir,
3895
+ });
3883
3896
  session = await loadSession({
3884
3897
  ...(yeaftDir && { dir: yeaftDir }),
3885
3898
  ...(normalizedWorkDir && { workDir: normalizedWorkDir }),
@@ -5626,8 +5639,6 @@ export async function handleYeaftLoadHistory(msg) {
5626
5639
  mode: 'recent',
5627
5640
  count: replayEntries.length,
5628
5641
  hasCompactSummary: hasCompactSummaryFlag,
5629
- totalHot: session.conversationStore.countHot(),
5630
- totalCold: session.conversationStore.countCold(),
5631
5642
  sessionId,
5632
5643
  hasMore,
5633
5644
  oldestSeq,
@@ -5642,12 +5653,22 @@ export async function handleYeaftLoadHistory(msg) {
5642
5653
  const limit = (typeof msg.limit === 'number') ? msg.limit : 10;
5643
5654
  ensureYeaftConversationId();
5644
5655
 
5656
+ let sessionMetaForRuntime = null;
5657
+ let sessionYeaftDir = yeaftDir;
5658
+ if (sessionId) {
5659
+ try {
5660
+ sessionYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
5661
+ const metaDir = join(sessionsRoot(sessionYeaftDir), sessionId);
5662
+ sessionMetaForRuntime = loadSessionMeta(metaDir);
5663
+ } catch { /* best-effort metadata hint */ }
5664
+ }
5665
+
5645
5666
  // First paint must not wait for full Yeaft runtime boot (MCP connects,
5646
- // skill scans, memory index sync). The conversation markdown store is the
5667
+ // skill scans, memory index sync). The conversation segment store is the
5647
5668
  // source of truth and can be opened cheaply, so replay the visible message
5648
5669
  // window immediately, then finish loadSession below for actual turns.
5649
5670
  const coldStoreStart = perfNowMs();
5650
- const coldStore = new ConversationStore(yeaftDir);
5671
+ const coldStore = new ConversationStore(sessionYeaftDir);
5651
5672
  traceDuration('history.cold_store_open', coldStoreStart);
5652
5673
  if (sessionId && (afterSeqRaw !== null || afterMessageId)) {
5653
5674
  let afterSeq = afterSeqRaw;
@@ -5677,14 +5698,6 @@ export async function handleYeaftLoadHistory(msg) {
5677
5698
  }
5678
5699
  historyAlreadyReplayed = true;
5679
5700
 
5680
- let sessionMetaForRuntime = null;
5681
- if (sessionId) {
5682
- try {
5683
- const groupYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
5684
- const metaDir = join(sessionsRoot(groupYeaftDir), sessionId);
5685
- sessionMetaForRuntime = loadSessionMeta(metaDir);
5686
- } catch { /* best-effort metadata hint */ }
5687
- }
5688
5701
  // Full runtime boot can be expensive (memory FTS sync, skills, MCP, dream
5689
5702
  // boot checks). It is not needed to render persisted history, so keep this
5690
5703
  // request short and let message-send await the same single-flight boot when