@yeaft/webchat-agent 1.0.78 → 1.0.79

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,24 +8,14 @@ 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
-
21
11
  /**
22
- * Search Markdown messages in a directory for a keyword.
12
+ * Search messages in a directory for a keyword.
23
13
  *
24
14
  * @param {string} dir — messages directory
25
15
  * @param {string} keyword — search term (case-insensitive)
26
16
  * @returns {object[]} — matching messages
27
17
  */
28
- function searchMarkdownDir(dir, keyword) {
18
+ function searchDir(dir, keyword) {
29
19
  if (!existsSync(dir)) return [];
30
20
 
31
21
  const lowerKeyword = keyword.toLowerCase();
@@ -45,29 +35,6 @@ function searchMarkdownDir(dir, keyword) {
45
35
  return results;
46
36
  }
47
37
 
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
-
71
38
  function compareNewest(a, b) {
72
39
  const sa = parseSeqFromId(a?.id);
73
40
  const sb = parseSeqFromId(b?.id);
@@ -75,7 +42,7 @@ function compareNewest(a, b) {
75
42
  return String(b?.time || '').localeCompare(String(a?.time || ''));
76
43
  }
77
44
 
78
- function sessionConversationDirs(dir) {
45
+ function sessionConversationMessageDirs(dir) {
79
46
  const dirs = [];
80
47
  const seen = new Set();
81
48
  for (const rootName of ['sessions', 'groups']) {
@@ -90,9 +57,12 @@ function sessionConversationDirs(dir) {
90
57
  }
91
58
 
92
59
  const conversationDir = join(sessionDir, 'conversation');
93
- if (seen.has(conversationDir)) continue;
94
- seen.add(conversationDir);
95
- dirs.push(conversationDir);
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
+ }
96
66
  }
97
67
  }
98
68
  return dirs;
@@ -109,23 +79,17 @@ function sessionConversationDirs(dir) {
109
79
  export function searchMessages(dir, keyword, limit = 20) {
110
80
  if (!keyword || !keyword.trim()) return [];
111
81
 
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')]),
82
+ const dirs = [
83
+ join(dir, 'chat', 'messages'),
84
+ join(dir, 'chat', 'cold'),
85
+ ...sessionConversationMessageDirs(dir),
119
86
  // Compatibility for profiles created before chat/session split.
120
87
  join(dir, 'conversation', 'messages'),
121
88
  join(dir, 'conversation', 'cold'),
122
89
  ];
123
- const segmentDirs = conversationDirs.map(d => join(d, 'segments'));
124
90
 
125
- return [
126
- ...segmentDirs.flatMap(d => searchSegmentDir(d, keyword)),
127
- ...markdownDirs.flatMap(d => searchMarkdownDir(d, keyword)),
128
- ]
91
+ return dirs
92
+ .flatMap(d => searchDir(d, keyword))
129
93
  .sort(compareNewest)
130
94
  .slice(0, limit);
131
95
  }
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, yeaftDirForWorkDir } from './sessions/session-crud.js';
48
+ import { ensureDefaultSessionIfEmpty } 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,7 +71,6 @@ 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
75
74
  * @property {string} [model] — Model override
76
75
  * @property {string} [language] — Language override ('en' | 'zh')
77
76
  * @property {boolean} [debug] — Debug mode override
@@ -137,7 +136,6 @@ function prepareToolStatsDir(yeaftDir) {
137
136
  export async function loadSession(options = {}) {
138
137
  const {
139
138
  dir,
140
- workDir,
141
139
  model,
142
140
  language,
143
141
  debug,
@@ -148,29 +146,21 @@ export async function loadSession(options = {}) {
148
146
  serverMode = false,
149
147
  } = options;
150
148
 
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.
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.
156
152
  const overrides = { ...configOverrides };
157
153
  if (dir) overrides.dir = dir;
158
154
  if (model) overrides.model = model;
159
155
  if (language) overrides.language = language;
160
156
  if (debug !== undefined) overrides.debug = debug;
161
157
 
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) {
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) {
174
164
  console.warn(`[Yeaft] ${w}`);
175
165
  }
176
166
 
@@ -222,7 +212,7 @@ export async function loadSession(options = {}) {
222
212
  // ─── 2a. Permission pre-check ─────────────────────────
223
213
  // If the data dir is not writable, mark session as read-only.
224
214
  // Persistence (conversation, memory, dream) is skipped in this mode.
225
- if (!storeInitResult.writable) {
215
+ if (!initResult.writable) {
226
216
  config._readOnly = true;
227
217
  console.warn(`[Yeaft] ${yeaftDir} is not writable — running in read-only mode`);
228
218
  }
@@ -375,23 +365,24 @@ export async function loadSession(options = {}) {
375
365
  }
376
366
 
377
367
  // ─── 6. Load skills ────────────────────────────────────
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();
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();
381
372
 
382
373
  let skillManager;
383
374
  if (skipSkills) {
384
375
  // Pass the literal user-tier dir (matches the normal branch's tier 2)
385
376
  // so any save/remove calls land in the same place users expect. New
386
377
  // `SkillManager` API takes literal scan dirs — no auto-suffix of /skills.
387
- skillManager = new SkillManager(join(configDir, 'skills'));
378
+ skillManager = new SkillManager(join(yeaftDir, 'skills'));
388
379
  // Don't call .load() — empty skill manager
389
380
  } else {
390
- skillManager = createSkillManager(configDir, projectTierRoot);
381
+ skillManager = createSkillManager(yeaftDir, projectTierRoot);
391
382
  }
392
383
 
393
384
  // ─── 7. Connect MCP servers ────────────────────────────
394
- const mcpConfig = loadMCPConfig(configDir, undefined, projectTierRoot);
385
+ const mcpConfig = loadMCPConfig(yeaftDir, undefined, projectTierRoot);
395
386
  const mcpManager = new MCPManager();
396
387
  let mcpStatus = { connected: [], failed: [] };
397
388
 
@@ -12,7 +12,10 @@
12
12
  *
13
13
  * task-330c lint guard:
14
14
  * ⚠️ DO NOT introduce greedy `text.replace(/---ROUTE---[\s\S]*$/g, '')`
15
- * style strips on incoming/outgoing message bodies.
15
+ * style strips on incoming/outgoing message bodies. Crew ROUTE
16
+ * stripping is owned EXCLUSIVELY by `agent/crew/routing.js`
17
+ * `parseRoutes()` which returns `{routes, displayBody}` with exact
18
+ * ranges removed.
16
19
  */
17
20
 
18
21
  import { delimiter, join } from 'node:path';
@@ -50,7 +53,6 @@ import {
50
53
  scanWorkdirSessions,
51
54
  restoreSessionToRegistry,
52
55
  readWorkDirRegistry,
53
- yeaftDirForWorkDir,
54
56
  } from './sessions/session-crud.js';
55
57
  import { openSession, loadSessionMeta } from './sessions/session-store.js';
56
58
  import { loadSessionConfig, resolveSessionConfig, SessionConfigError } from './sessions/session-config.js';
@@ -412,13 +414,6 @@ function projectRuntimeKey(workDir) {
412
414
  return normalizeSessionWorkDir(workDir) || '__agent_cwd__';
413
415
  }
414
416
 
415
- function resolveStoreYeaftDirForSession(defaultYeaftDir, { sessionId = null, sessionMeta = null, workDir = '' } = {}) {
416
- const normalizedWorkDir = normalizeSessionWorkDir(workDir || sessionMeta?.workDir);
417
- if (normalizedWorkDir) return yeaftDirForWorkDir(normalizedWorkDir);
418
- if (sessionId) return resolveSessionYeaftDir(defaultYeaftDir, sessionId);
419
- return defaultYeaftDir;
420
- }
421
-
422
417
  function createThreadId() {
423
418
  return `thr_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
424
419
  }
@@ -3588,7 +3583,7 @@ async function runYeaftSessionSend(msg) {
3588
3583
 
3589
3584
  // ── Attachments (images + files) ───────────────────────────────
3590
3585
  // Server has already resolved fileId → { name, mimeType, data:base64,
3591
- // isImage } via the client-conversation.js relay
3586
+ // isImage } via the same path crew uses (client-conversation.js relay
3592
3587
  // for `yeaft_*`). We persist files to disk under the agent's CWD so
3593
3588
  // file-tools (file-read / bash) can pick them up with relative paths,
3594
3589
  // and we build per-image content blocks for the LLM call. The
@@ -3885,11 +3880,6 @@ async function ensureSessionLoaded(opts = {}) {
3885
3880
  sessionLoadPromise = (async () => {
3886
3881
  const yeaftDir = ctx.CONFIG?.yeaftDir;
3887
3882
  const normalizedWorkDir = normalizeSessionWorkDir(opts?.workDir || opts?.sessionMeta?.workDir);
3888
- const sessionYeaftDir = resolveStoreYeaftDirForSession(yeaftDir, {
3889
- sessionId: opts?.sessionId || opts?.sessionMeta?.id || null,
3890
- sessionMeta: opts?.sessionMeta || null,
3891
- workDir: normalizedWorkDir,
3892
- });
3893
3883
  session = await loadSession({
3894
3884
  ...(yeaftDir && { dir: yeaftDir }),
3895
3885
  ...(normalizedWorkDir && { workDir: normalizedWorkDir }),
@@ -5650,22 +5640,12 @@ export async function handleYeaftLoadHistory(msg) {
5650
5640
  const limit = (typeof msg.limit === 'number') ? msg.limit : 10;
5651
5641
  ensureYeaftConversationId();
5652
5642
 
5653
- let sessionMetaForRuntime = null;
5654
- let sessionYeaftDir = yeaftDir;
5655
- if (sessionId) {
5656
- try {
5657
- sessionYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
5658
- const metaDir = join(sessionsRoot(sessionYeaftDir), sessionId);
5659
- sessionMetaForRuntime = loadSessionMeta(metaDir);
5660
- } catch { /* best-effort metadata hint */ }
5661
- }
5662
-
5663
5643
  // First paint must not wait for full Yeaft runtime boot (MCP connects,
5664
- // skill scans, memory index sync). The conversation segment store is the
5644
+ // skill scans, memory index sync). The conversation markdown store is the
5665
5645
  // source of truth and can be opened cheaply, so replay the visible message
5666
5646
  // window immediately, then finish loadSession below for actual turns.
5667
5647
  const coldStoreStart = perfNowMs();
5668
- const coldStore = new ConversationStore(sessionYeaftDir);
5648
+ const coldStore = new ConversationStore(yeaftDir);
5669
5649
  traceDuration('history.cold_store_open', coldStoreStart);
5670
5650
  if (sessionId && (afterSeqRaw !== null || afterMessageId)) {
5671
5651
  let afterSeq = afterSeqRaw;
@@ -5695,6 +5675,14 @@ export async function handleYeaftLoadHistory(msg) {
5695
5675
  }
5696
5676
  historyAlreadyReplayed = true;
5697
5677
 
5678
+ let sessionMetaForRuntime = null;
5679
+ if (sessionId) {
5680
+ try {
5681
+ const groupYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
5682
+ const metaDir = join(sessionsRoot(groupYeaftDir), sessionId);
5683
+ sessionMetaForRuntime = loadSessionMeta(metaDir);
5684
+ } catch { /* best-effort metadata hint */ }
5685
+ }
5698
5686
  // Full runtime boot can be expensive (memory FTS sync, skills, MCP, dream
5699
5687
  // boot checks). It is not needed to render persisted history, so keep this
5700
5688
  // request short and let message-send await the same single-flight boot when