@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.
- package/package.json +1 -1
- package/yeaft/conversation/persist.js +626 -163
- package/yeaft/conversation/search.js +51 -15
- package/yeaft/session.js +27 -18
- package/yeaft/web-bridge.js +25 -12
|
@@ -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
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
83
|
-
join(dir, 'chat'
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
92
|
-
.flatMap(d =>
|
|
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
|
|
150
|
-
// Must happen BEFORE loadConfig so that first-run
|
|
151
|
-
//
|
|
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
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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 (!
|
|
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
|
-
//
|
|
369
|
-
//
|
|
370
|
-
|
|
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(
|
|
387
|
+
skillManager = new SkillManager(join(configDir, 'skills'));
|
|
379
388
|
// Don't call .load() — empty skill manager
|
|
380
389
|
} else {
|
|
381
|
-
skillManager = createSkillManager(
|
|
390
|
+
skillManager = createSkillManager(configDir, projectTierRoot);
|
|
382
391
|
}
|
|
383
392
|
|
|
384
393
|
// ─── 7. Connect MCP servers ────────────────────────────
|
|
385
|
-
const mcpConfig = loadMCPConfig(
|
|
394
|
+
const mcpConfig = loadMCPConfig(configDir, undefined, projectTierRoot);
|
|
386
395
|
const mcpManager = new MCPManager();
|
|
387
396
|
let mcpStatus = { connected: [], failed: [] };
|
|
388
397
|
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|