@yeaft/webchat-agent 1.0.88 → 1.0.89
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/session.js +25 -10
- package/yeaft/sessions/session-config.js +8 -9
- package/yeaft/sessions/session-crud.js +77 -89
- package/yeaft/web-bridge.js +32 -42
package/package.json
CHANGED
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,
|
|
48
|
+
import { ensureDefaultSessionIfEmpty, migrateRegisteredWorkDirSessions } 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,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;
|
|
74
|
+
* @property {string} [workDir] — Session workDir; used only for project-tier assets such as skills/MCP, while Session data stays under the user-level dir
|
|
75
75
|
* @property {string} [model] — Model override
|
|
76
76
|
* @property {string} [language] — Language override ('en' | 'zh')
|
|
77
77
|
* @property {boolean} [debug] — Debug mode override
|
|
@@ -150,9 +150,10 @@ export async function loadSession(options = {}) {
|
|
|
150
150
|
|
|
151
151
|
// ─── 1. Determine config + store directories ─────────────
|
|
152
152
|
// Must happen BEFORE loadConfig so that first-run generates a
|
|
153
|
-
// default config.json that loadConfig can read.
|
|
154
|
-
//
|
|
155
|
-
//
|
|
153
|
+
// default config.json that loadConfig can read. Session data
|
|
154
|
+
// (metadata/roster/config/messages/memory/tasks) is agent-user data
|
|
155
|
+
// and stays under configDir (`~/.yeaft`). workDir is only a project
|
|
156
|
+
// tier for assets such as skills and MCP config.
|
|
156
157
|
const overrides = { ...configOverrides };
|
|
157
158
|
if (dir) overrides.dir = dir;
|
|
158
159
|
if (model) overrides.model = model;
|
|
@@ -161,15 +162,13 @@ export async function loadSession(options = {}) {
|
|
|
161
162
|
|
|
162
163
|
const sessionWorkDir = typeof workDir === 'string' && workDir.trim() ? workDir.trim() : '';
|
|
163
164
|
const configDir = overrides.dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
164
|
-
const yeaftDir =
|
|
165
|
+
const yeaftDir = configDir;
|
|
165
166
|
const configInitResult = initYeaftDir(configDir);
|
|
166
|
-
const storeInitResult =
|
|
167
|
+
const storeInitResult = configInitResult;
|
|
167
168
|
overrides.dir = configDir;
|
|
168
169
|
|
|
169
170
|
// Log any warnings from directory initialization.
|
|
170
|
-
const initWarnings =
|
|
171
|
-
? configInitResult.warnings
|
|
172
|
-
: [...configInitResult.warnings, ...storeInitResult.warnings];
|
|
171
|
+
const initWarnings = configInitResult.warnings;
|
|
173
172
|
for (const w of initWarnings) {
|
|
174
173
|
console.warn(`[Yeaft] ${w}`);
|
|
175
174
|
}
|
|
@@ -219,6 +218,22 @@ export async function loadSession(options = {}) {
|
|
|
219
218
|
// dream directly. Existing users have already migrated
|
|
220
219
|
// (state file in ~/.yeaft/.memory-v2-migration.json).
|
|
221
220
|
|
|
221
|
+
// ─── 2.3 Project-session migration ─────────────────────
|
|
222
|
+
// Session data used to be allowed under `<workDir>/.yeaft/sessions`.
|
|
223
|
+
// It is now user-level only (`~/.yeaft/sessions`). Copy any registered
|
|
224
|
+
// project-backed sessions into the user root before stores open.
|
|
225
|
+
try {
|
|
226
|
+
const migration = migrateRegisteredWorkDirSessions(configDir);
|
|
227
|
+
if (migration.migrated.length > 0 || migration.errors.length > 0) {
|
|
228
|
+
console.log(
|
|
229
|
+
`[Yeaft] session project-store migration: migrated=${migration.migrated.length} ` +
|
|
230
|
+
`errors=${migration.errors.length}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
} catch (err) {
|
|
234
|
+
console.warn(`[Yeaft] session project-store migration failed: ${err?.message || err}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
222
237
|
// ─── 2a. Permission pre-check ─────────────────────────
|
|
223
238
|
// If the data dir is not writable, mark session as read-only.
|
|
224
239
|
// Persistence (conversation, memory, dream) is skipped in this mode.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* session-config.js — Per-session selected model override state.
|
|
3
3
|
*
|
|
4
|
-
* Each session
|
|
4
|
+
* Each session carries its header-selected model override in `config.json` at
|
|
5
5
|
* ~/.yeaft/sessions/<sessionId>/config.json
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
*
|
|
7
|
+
* Session config is user-level state. Project `.yeaft` directories may provide
|
|
8
|
+
* project assets such as skills/MCP config, but must not own Session config.
|
|
8
9
|
*
|
|
9
10
|
* v1 schema (intentionally tiny — extend via additive keys only):
|
|
10
11
|
* {
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
import { existsSync, readFileSync } from 'fs';
|
|
25
26
|
import { join } from 'path';
|
|
26
27
|
import { writeAtomic } from '../storage/index.js';
|
|
27
|
-
import { sessionsRoot
|
|
28
|
+
import { sessionsRoot } from './session-crud.js';
|
|
28
29
|
|
|
29
30
|
const CONFIG_FILE = 'config.json';
|
|
30
31
|
|
|
@@ -41,14 +42,12 @@ export class SessionConfigError extends Error {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
|
-
* Resolve the on-disk path for a session's config.json.
|
|
45
|
-
*
|
|
46
|
-
* keep their config alongside the session metadata.
|
|
45
|
+
* Resolve the on-disk path for a session's config.json. Always uses the
|
|
46
|
+
* agent-local Yeaft root (`~/.yeaft` in production), not a project `.yeaft`.
|
|
47
47
|
*/
|
|
48
48
|
export function sessionConfigPath(yeaftDir, sessionId) {
|
|
49
49
|
if (!yeaftDir) return null;
|
|
50
|
-
|
|
51
|
-
return join(sessionsRoot(sessionYeaftDir), sessionId, CONFIG_FILE);
|
|
50
|
+
return join(sessionsRoot(yeaftDir), sessionId, CONFIG_FILE);
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
/**
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
existsSync,
|
|
39
39
|
renameSync,
|
|
40
40
|
rmSync,
|
|
41
|
+
cpSync,
|
|
41
42
|
readdirSync,
|
|
42
43
|
statSync,
|
|
43
44
|
mkdirSync,
|
|
@@ -156,101 +157,94 @@ export function unregisterSessionWorkDir(defaultYeaftDir, sessionId) {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
160
|
+
* One-way compatibility migration for sessions previously stored under a
|
|
161
|
+
* project `.yeaft/sessions` tree. New reads/writes never use that location, but
|
|
162
|
+
* older installs may still have their only `session.json` + conversation jsonl
|
|
163
|
+
* there and a `group-workdirs.json` registry entry pointing to it.
|
|
161
164
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* concern owned by the handler / caller (see
|
|
166
|
-
* `handleYeaftScanWorkdirSessions`). Keeping the utility layer-pure makes
|
|
167
|
-
* it usable from contexts that don't have / don't care about the registry
|
|
168
|
-
* (e.g. CLI tools, future per-workdir snapshots).
|
|
165
|
+
* We copy missing session directories into the user-level root and never
|
|
166
|
+
* overwrite an existing user-level session. The old project directory is left in
|
|
167
|
+
* place as a safety backup; subsequent runtime paths ignore it.
|
|
169
168
|
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
|
|
169
|
+
* @param {string} yeaftDir user-level Yeaft root
|
|
170
|
+
* @returns {{migrated:string[], skipped:string[], errors:Array<{sessionId:string,error:string}>}}
|
|
171
|
+
*/
|
|
172
|
+
export function migrateRegisteredWorkDirSessions(yeaftDir) {
|
|
173
|
+
const result = { migrated: [], skipped: [], errors: [] };
|
|
174
|
+
if (!yeaftDir) return result;
|
|
175
|
+
const registry = readWorkDirRegistry(yeaftDir);
|
|
176
|
+
const userRoot = sessionsRoot(yeaftDir);
|
|
177
|
+
for (const [sessionId, workDir] of Object.entries(registry)) {
|
|
178
|
+
const normalized = normalizeWorkDir(workDir);
|
|
179
|
+
if (!sessionId || !normalized) continue;
|
|
180
|
+
const sourceDir = join(sessionsRoot(yeaftDirForWorkDir(normalized)), sessionId);
|
|
181
|
+
const targetDir = join(userRoot, sessionId);
|
|
182
|
+
let tmpDir = null;
|
|
183
|
+
try {
|
|
184
|
+
if (existsSync(targetDir)) {
|
|
185
|
+
const targetMeta = loadSessionMeta(targetDir);
|
|
186
|
+
if (targetMeta) {
|
|
187
|
+
result.skipped.push(sessionId);
|
|
188
|
+
} else {
|
|
189
|
+
result.errors.push({ sessionId, error: 'target session directory exists but session metadata is invalid' });
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const sourceMeta = existsSync(sourceDir) ? loadSessionMeta(sourceDir) : null;
|
|
194
|
+
if (!sourceMeta) {
|
|
195
|
+
result.skipped.push(sessionId);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
mkdirSync(userRoot, { recursive: true });
|
|
199
|
+
tmpDir = join(userRoot, `.migrating-${sessionId}-${randomBytes(6).toString('hex')}`);
|
|
200
|
+
cpSync(sourceDir, tmpDir, { recursive: true, errorOnExist: true });
|
|
201
|
+
const copiedMeta = loadSessionMeta(tmpDir);
|
|
202
|
+
if (!copiedMeta || copiedMeta.id !== sessionId) {
|
|
203
|
+
throw new Error('copied session metadata failed validation');
|
|
204
|
+
}
|
|
205
|
+
renameSync(tmpDir, targetDir);
|
|
206
|
+
tmpDir = null;
|
|
207
|
+
result.migrated.push(sessionId);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
210
|
+
result.errors.push({ sessionId, error: err?.message || String(err) });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Workdir Session storage was retired: Session metadata/roster/config/history
|
|
218
|
+
* live under the agent-local Yeaft root (`~/.yeaft/sessions`), not under a
|
|
219
|
+
* project `.yeaft/sessions` tree. Keep this helper as a compatibility no-op for
|
|
220
|
+
* older UI flows that may still ask to scan a workdir.
|
|
173
221
|
*
|
|
174
222
|
* @param {string} workDir — the working directory to scan.
|
|
175
223
|
* @returns {Array<object>}
|
|
176
224
|
*/
|
|
177
225
|
export function scanWorkdirSessions(workDir) {
|
|
178
|
-
|
|
179
|
-
if (!normalized) return [];
|
|
180
|
-
const groupYeaftDir = yeaftDirForWorkDir(normalized);
|
|
181
|
-
const root = sessionsRoot(groupYeaftDir);
|
|
182
|
-
if (!existsSync(root)) return [];
|
|
183
|
-
const out = [];
|
|
184
|
-
let entries;
|
|
185
|
-
try {
|
|
186
|
-
entries = readdirSync(root);
|
|
187
|
-
} catch {
|
|
188
|
-
return [];
|
|
189
|
-
}
|
|
190
|
-
for (const name of entries) {
|
|
191
|
-
if (name.startsWith('.')) continue; // skip .archived-* and dotfiles
|
|
192
|
-
const dir = join(root, name);
|
|
193
|
-
try { if (!statSync(dir).isDirectory()) continue; } catch { continue; }
|
|
194
|
-
const meta = loadSessionMeta(dir);
|
|
195
|
-
if (!meta) continue;
|
|
196
|
-
out.push({
|
|
197
|
-
...meta,
|
|
198
|
-
workDir: normalized,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
return out.sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || '')));
|
|
226
|
+
return [];
|
|
202
227
|
}
|
|
203
228
|
|
|
204
229
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* Validates that `<workDir>/.yeaft/sessions/<sessionId>/session.json`
|
|
209
|
-
* exists and is parseable, with legacy `group.json` as a read fallback.
|
|
210
|
-
* Throws:
|
|
211
|
-
* - `not_found` — the session dir is not on disk at this workdir
|
|
212
|
-
* - `corrupt_meta` — the dir exists but session metadata is missing /
|
|
213
|
-
* unreadable / can't be parsed. Surfaced as a distinct
|
|
214
|
-
* code so the UI can tell the user "the file is broken"
|
|
215
|
-
* instead of "you picked the wrong workdir" (review
|
|
216
|
-
* finding I1).
|
|
217
|
-
*
|
|
218
|
-
* Idempotent: if the same `(sessionId, workDir)` is already registered,
|
|
219
|
-
* we still rewrite the entry (with the normalized path) and return the
|
|
220
|
-
* fresh meta — no error.
|
|
230
|
+
* Compatibility shim for the retired workdir-restore flow. Session metadata is
|
|
231
|
+
* no longer accepted from project `.yeaft/sessions`; users should create or load
|
|
232
|
+
* Sessions from the user-level `~/.yeaft/sessions` store.
|
|
221
233
|
*
|
|
222
234
|
* @param {string} defaultYeaftDir
|
|
223
235
|
* @param {string} sessionId
|
|
224
236
|
* @param {string} workDir
|
|
225
|
-
* @returns {object}
|
|
237
|
+
* @returns {object}
|
|
226
238
|
*/
|
|
227
239
|
export function restoreSessionToRegistry(defaultYeaftDir, sessionId, workDir) {
|
|
228
240
|
if (!sessionId) throw new SessionCrudError('invalid_session_id', null);
|
|
229
|
-
|
|
230
|
-
if (!normalized) throw new SessionCrudError('invalid_workdir', sessionId);
|
|
231
|
-
const groupYeaftDir = yeaftDirForWorkDir(normalized);
|
|
232
|
-
const dir = join(sessionsRoot(groupYeaftDir), sessionId);
|
|
233
|
-
if (!existsSync(dir)) throw new SessionCrudError('not_found', sessionId);
|
|
234
|
-
const meta = loadSessionMeta(dir);
|
|
235
|
-
if (!meta) throw new SessionCrudError('corrupt_meta', sessionId, `session metadata missing or unreadable at ${dir} (expected session.json or legacy group.json)`);
|
|
236
|
-
registerSessionWorkDir(defaultYeaftDir, sessionId, normalized);
|
|
237
|
-
return { ...meta, workDir: normalized };
|
|
241
|
+
throw new SessionCrudError('not_found', sessionId, 'workdir Session restore is retired; Sessions are stored under the user-level Yeaft folder');
|
|
238
242
|
}
|
|
239
243
|
|
|
240
244
|
export function resolveSessionYeaftDir(defaultYeaftDir, sessionId) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const workDir = normalizeWorkDir(registry[sessionId]);
|
|
245
|
-
if (workDir) {
|
|
246
|
-
const candidate = yeaftDirForWorkDir(workDir);
|
|
247
|
-
const candidateDir = join(sessionsRoot(candidate), sessionId);
|
|
248
|
-
if (existsSync(candidateDir) && loadSessionMeta(candidateDir)) return candidate;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const defaultSessionDir = join(sessionsRoot(defaultYeaftDir), sessionId);
|
|
252
|
-
if (existsSync(defaultSessionDir) && loadSessionMeta(defaultSessionDir)) return defaultYeaftDir;
|
|
253
|
-
|
|
245
|
+
// Legacy helper retained for old call sites. Session storage now always
|
|
246
|
+
// resolves to the agent-local root; project `.yeaft` is only for project
|
|
247
|
+
// assets such as skills/MCP, never Session data.
|
|
254
248
|
return defaultYeaftDir;
|
|
255
249
|
}
|
|
256
250
|
|
|
@@ -328,8 +322,7 @@ export function ensureDefaultSessionIfEmpty(yeaftDir, options = {}) {
|
|
|
328
322
|
export function createSessionFromSpec(yeaftDir, spec, options = {}) {
|
|
329
323
|
const input = spec || {};
|
|
330
324
|
const normalizedWorkDir = normalizeWorkDir(input.workDir);
|
|
331
|
-
const
|
|
332
|
-
const memoryRoot = options.memoryRoot || (groupYeaftDir ? join(groupYeaftDir, 'memory') : DEFAULT_MEMORY_ROOT);
|
|
325
|
+
const memoryRoot = options.memoryRoot || (yeaftDir ? join(yeaftDir, 'memory') : DEFAULT_MEMORY_ROOT);
|
|
333
326
|
const libDir = options.libDir || DEFAULT_VP_LIB_DIR;
|
|
334
327
|
const name = String(input.name || '').trim();
|
|
335
328
|
if (!name) throw new SessionCrudError('invalid_name', null, 'group name required');
|
|
@@ -355,7 +348,7 @@ export function createSessionFromSpec(yeaftDir, spec, options = {}) {
|
|
|
355
348
|
if (!defaultVpId) defaultVpId = roster[0] || null;
|
|
356
349
|
|
|
357
350
|
const id = makeSessionId(name);
|
|
358
|
-
const root = sessionsRoot(
|
|
351
|
+
const root = sessionsRoot(yeaftDir);
|
|
359
352
|
if (existsSync(join(root, id))) {
|
|
360
353
|
// Extremely unlikely (ulid suffix), but surface deterministically.
|
|
361
354
|
throw new SessionCrudError('duplicate', id);
|
|
@@ -456,8 +449,7 @@ export function updateSessionConfig(yeaftDir, sessionId, partial) {
|
|
|
456
449
|
* own second-confirm modal (acceptance #4 in task-334-slice-specs.md 334m).
|
|
457
450
|
*/
|
|
458
451
|
export function archiveSession(yeaftDir, sessionId) {
|
|
459
|
-
const
|
|
460
|
-
const root = sessionsRoot(groupYeaftDir);
|
|
452
|
+
const root = sessionsRoot(yeaftDir);
|
|
461
453
|
const srcDir = join(root, sessionId);
|
|
462
454
|
// Idempotent — nothing on disk, nothing to archive. Workdir-registry is
|
|
463
455
|
// still cleared in case it points at a stale row.
|
|
@@ -487,9 +479,8 @@ export function archiveSession(yeaftDir, sessionId) {
|
|
|
487
479
|
* delete cleans up legacy state too.
|
|
488
480
|
*/
|
|
489
481
|
export function deleteSession(yeaftDir, sessionId, options = {}) {
|
|
490
|
-
const
|
|
491
|
-
const
|
|
492
|
-
const root = sessionsRoot(groupYeaftDir);
|
|
482
|
+
const memoryRoot = options.memoryRoot || (yeaftDir ? join(yeaftDir, 'memory') : DEFAULT_MEMORY_ROOT);
|
|
483
|
+
const root = sessionsRoot(yeaftDir);
|
|
493
484
|
const srcDir = join(root, sessionId);
|
|
494
485
|
const liveExists = existsSync(srcDir) && !!loadSessionMeta(srcDir);
|
|
495
486
|
|
|
@@ -610,8 +601,7 @@ export function setSessionDefaultVp(yeaftDir, sessionId, vpId) {
|
|
|
610
601
|
}
|
|
611
602
|
|
|
612
603
|
export function requireSession(yeaftDir, sessionId) {
|
|
613
|
-
const
|
|
614
|
-
const root = sessionsRoot(groupYeaftDir);
|
|
604
|
+
const root = sessionsRoot(yeaftDir);
|
|
615
605
|
const dir = join(root, sessionId);
|
|
616
606
|
if (!existsSync(dir) || !loadSessionMeta(dir)) {
|
|
617
607
|
throw new SessionCrudError('not_found', sessionId);
|
|
@@ -627,10 +617,8 @@ export function snapshotSessions(yeaftDir) {
|
|
|
627
617
|
}
|
|
628
618
|
const registry = readWorkDirRegistry(yeaftDir);
|
|
629
619
|
for (const [sessionId, workDir] of Object.entries(registry)) {
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
const meta = existsSync(dir) ? loadSessionMeta(dir) : null;
|
|
633
|
-
if (meta) byId.set(meta.id, meta);
|
|
620
|
+
const meta = byId.get(sessionId);
|
|
621
|
+
if (meta && !meta.workDir) meta.workDir = normalizeWorkDir(workDir);
|
|
634
622
|
}
|
|
635
623
|
// Attach per-group config overrides (v1: just `model`). Frontend can
|
|
636
624
|
// render the effective model without re-querying.
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -45,12 +45,11 @@ import {
|
|
|
45
45
|
removeMember,
|
|
46
46
|
setSessionDefaultVp,
|
|
47
47
|
snapshotSessions,
|
|
48
|
-
resolveSessionYeaftDir,
|
|
49
48
|
sessionsRoot,
|
|
50
49
|
scanWorkdirSessions,
|
|
51
50
|
restoreSessionToRegistry,
|
|
52
51
|
readWorkDirRegistry,
|
|
53
|
-
|
|
52
|
+
migrateRegisteredWorkDirSessions,
|
|
54
53
|
} from './sessions/session-crud.js';
|
|
55
54
|
import { openSession, loadSessionMeta } from './sessions/session-store.js';
|
|
56
55
|
import { loadSessionConfig, resolveSessionConfig, SessionConfigError } from './sessions/session-config.js';
|
|
@@ -189,8 +188,8 @@ function scheduleYeaftLoadHistoryMetadataReplay(sessionId) {
|
|
|
189
188
|
let projectRuntime = null;
|
|
190
189
|
if (sessionId) {
|
|
191
190
|
try {
|
|
192
|
-
const
|
|
193
|
-
const meta = loadSessionMeta(join(sessionsRoot(
|
|
191
|
+
const metaRoot = ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
192
|
+
const meta = loadSessionMeta(join(sessionsRoot(metaRoot), sessionId));
|
|
194
193
|
projectRuntime = await ensureProjectRuntimeForSessionMeta(meta);
|
|
195
194
|
} catch { /* best-effort project metadata */ }
|
|
196
195
|
}
|
|
@@ -429,13 +428,6 @@ function projectRuntimeKey(workDir) {
|
|
|
429
428
|
return normalizeSessionWorkDir(workDir) || '__agent_cwd__';
|
|
430
429
|
}
|
|
431
430
|
|
|
432
|
-
function resolveStoreYeaftDirForSession(defaultYeaftDir, { sessionId = null, sessionMeta = null, workDir = '' } = {}) {
|
|
433
|
-
const normalizedWorkDir = normalizeSessionWorkDir(workDir || sessionMeta?.workDir);
|
|
434
|
-
if (normalizedWorkDir) return yeaftDirForWorkDir(normalizedWorkDir);
|
|
435
|
-
if (sessionId) return resolveSessionYeaftDir(defaultYeaftDir, sessionId);
|
|
436
|
-
return defaultYeaftDir;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
431
|
function createThreadId() {
|
|
440
432
|
return `thr_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
|
|
441
433
|
}
|
|
@@ -1315,10 +1307,8 @@ function getOrCreateVpEngine(sessionId, vpId, threadId = 'main') {
|
|
|
1315
1307
|
const key = threadKey(sessionId, vpId, threadId);
|
|
1316
1308
|
if (!session) throw new Error('getOrCreateVpEngine: session not loaded');
|
|
1317
1309
|
// Per-session config overlay (v1: model only). Falls back to the
|
|
1318
|
-
// session's user-level config when no override is set.
|
|
1319
|
-
//
|
|
1320
|
-
// sessions from there, while still allowing a later agent-local session in
|
|
1321
|
-
// the same bridge runtime to read its own override after a workDir-first boot.
|
|
1310
|
+
// session's user-level config when no override is set. Session config is
|
|
1311
|
+
// always resolved from the agent-local root; project `.yeaft` is ignored.
|
|
1322
1312
|
const sessionConfigRoot = liveConfigRoot();
|
|
1323
1313
|
const groupCfg = loadSessionConfig(sessionConfigRoot, sessionId);
|
|
1324
1314
|
const effectiveConfig = resolveSessionConfig(session.config, groupCfg);
|
|
@@ -2465,14 +2455,10 @@ export function handleYeaftCreateSession(msg) {
|
|
|
2465
2455
|
}
|
|
2466
2456
|
|
|
2467
2457
|
/**
|
|
2468
|
-
* `yeaft_scan_workdir_sessions` —
|
|
2469
|
-
*
|
|
2470
|
-
*
|
|
2471
|
-
*
|
|
2472
|
-
* throws on missing/empty directories.
|
|
2473
|
-
*
|
|
2474
|
-
* Pairs with `handleYeaftRestoreSession` for the "Restore session from a
|
|
2475
|
-
* workdir" flow — see plan rosy-snuggling-waterfall.md.
|
|
2458
|
+
* `yeaft_scan_workdir_sessions` — compatibility endpoint for the retired
|
|
2459
|
+
* workdir Session scan flow. Session data now lives under the user-level
|
|
2460
|
+
* `~/.yeaft/sessions`; project `.yeaft` is reserved for project assets such as
|
|
2461
|
+
* skills and MCP config.
|
|
2476
2462
|
*/
|
|
2477
2463
|
export function handleYeaftScanWorkdirSessions(msg) {
|
|
2478
2464
|
const requestId = msg && msg.requestId;
|
|
@@ -2480,11 +2466,8 @@ export function handleYeaftScanWorkdirSessions(msg) {
|
|
|
2480
2466
|
const workDir = String(msg && msg.workDir || '').trim();
|
|
2481
2467
|
if (!workDir) throw new SessionCrudError('invalid_workdir', null, 'workDir required');
|
|
2482
2468
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
2483
|
-
// scanWorkdirSessions is a
|
|
2484
|
-
//
|
|
2485
|
-
// `alreadyRegistered` because that flag couples the per-workdir scan
|
|
2486
|
-
// to a specific agent's registry (the same scan from a CLI tool or
|
|
2487
|
-
// a different agent would compare against a different registry).
|
|
2469
|
+
// scanWorkdirSessions is now a compatibility no-op because Session data is
|
|
2470
|
+
// user-level only. Keep the decoration shape stable for older clients.
|
|
2488
2471
|
const sessions = scanWorkdirSessions(workDir);
|
|
2489
2472
|
const registry = readWorkDirRegistry(yeaftDir);
|
|
2490
2473
|
const decorated = sessions.map(s => ({
|
|
@@ -3533,8 +3516,8 @@ async function runYeaftSessionSend(msg) {
|
|
|
3533
3516
|
return;
|
|
3534
3517
|
}
|
|
3535
3518
|
|
|
3536
|
-
// Open the session metadata first so
|
|
3537
|
-
//
|
|
3519
|
+
// Open the user-level session metadata first so project runtime loading can
|
|
3520
|
+
// use the stored workDir without blocking the user-message hot path. If
|
|
3538
3521
|
// metadata is missing we still boot the agent runtime for a useful error
|
|
3539
3522
|
// response, then fail on the session open check.
|
|
3540
3523
|
let sessionHandle = null;
|
|
@@ -3542,8 +3525,7 @@ async function runYeaftSessionSend(msg) {
|
|
|
3542
3525
|
let sessionMetaForRuntime = null;
|
|
3543
3526
|
const openSessionStart = perfNowMs();
|
|
3544
3527
|
try {
|
|
3545
|
-
|
|
3546
|
-
sessionRoot = sessionsRoot(groupYeaftDir);
|
|
3528
|
+
sessionRoot = sessionsRoot(yeaftDir);
|
|
3547
3529
|
const dir = join(sessionRoot, sessionId);
|
|
3548
3530
|
if (existsSync(dir) && loadSessionMeta(dir)) {
|
|
3549
3531
|
sessionHandle = openSession(sessionRoot, sessionId);
|
|
@@ -3564,6 +3546,17 @@ async function runYeaftSessionSend(msg) {
|
|
|
3564
3546
|
|
|
3565
3547
|
const ensureSessionStart = perfNowMs();
|
|
3566
3548
|
await ensureSessionLoaded({ sessionMeta: sessionMetaForRuntime, perfTraceId });
|
|
3549
|
+
if (!sessionHandle) {
|
|
3550
|
+
try {
|
|
3551
|
+
const dir = join(sessionRoot, sessionId);
|
|
3552
|
+
if (existsSync(dir) && loadSessionMeta(dir)) {
|
|
3553
|
+
sessionHandle = openSession(sessionRoot, sessionId);
|
|
3554
|
+
sessionMetaForRuntime = sessionHandle.getMeta();
|
|
3555
|
+
}
|
|
3556
|
+
} catch (err) {
|
|
3557
|
+
console.warn('[Yeaft] yeaft_session_chat: migrated session reopen failed', err?.message || err);
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3567
3560
|
scheduleProjectRuntimeLoad(sessionMetaForRuntime?.workDir);
|
|
3568
3561
|
traceDuration('session_send.ensure_session_loaded', ensureSessionStart);
|
|
3569
3562
|
|
|
@@ -3927,11 +3920,6 @@ async function ensureSessionLoaded(opts = {}) {
|
|
|
3927
3920
|
sessionLoadPromise = (async () => {
|
|
3928
3921
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
3929
3922
|
const normalizedWorkDir = normalizeSessionWorkDir(opts?.workDir || opts?.sessionMeta?.workDir);
|
|
3930
|
-
const sessionYeaftDir = resolveStoreYeaftDirForSession(yeaftDir, {
|
|
3931
|
-
sessionId: opts?.sessionId || opts?.sessionMeta?.id || null,
|
|
3932
|
-
sessionMeta: opts?.sessionMeta || null,
|
|
3933
|
-
workDir: normalizedWorkDir,
|
|
3934
|
-
});
|
|
3935
3923
|
session = await loadSession({
|
|
3936
3924
|
...(yeaftDir && { dir: yeaftDir }),
|
|
3937
3925
|
...(normalizedWorkDir && { workDir: normalizedWorkDir }),
|
|
@@ -5694,13 +5682,15 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
5694
5682
|
ensureYeaftConversationId();
|
|
5695
5683
|
|
|
5696
5684
|
let sessionMetaForRuntime = null;
|
|
5697
|
-
|
|
5685
|
+
const historyYeaftDir = yeaftDir;
|
|
5698
5686
|
if (sessionId) {
|
|
5699
5687
|
try {
|
|
5700
|
-
|
|
5701
|
-
const metaDir = join(sessionsRoot(
|
|
5688
|
+
migrateRegisteredWorkDirSessions(yeaftDir);
|
|
5689
|
+
const metaDir = join(sessionsRoot(yeaftDir), sessionId);
|
|
5702
5690
|
sessionMetaForRuntime = loadSessionMeta(metaDir);
|
|
5703
|
-
} catch {
|
|
5691
|
+
} catch (err) {
|
|
5692
|
+
console.warn('[Yeaft] load_history project-store migration failed:', err?.message || err);
|
|
5693
|
+
}
|
|
5704
5694
|
}
|
|
5705
5695
|
|
|
5706
5696
|
// First paint must not wait for full Yeaft runtime boot (MCP connects,
|
|
@@ -5708,7 +5698,7 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
5708
5698
|
// source of truth and can be opened cheaply, so replay the visible message
|
|
5709
5699
|
// window immediately, then finish loadSession below for actual turns.
|
|
5710
5700
|
const coldStoreStart = perfNowMs();
|
|
5711
|
-
const coldStore = new ConversationStore(
|
|
5701
|
+
const coldStore = new ConversationStore(historyYeaftDir);
|
|
5712
5702
|
traceDuration('history.cold_store_open', coldStoreStart);
|
|
5713
5703
|
if (sessionId && (afterSeqRaw !== null || afterMessageId)) {
|
|
5714
5704
|
let afterSeq = afterSeqRaw;
|